toeverything/AFFiNE · error · BlockSuiteError

ReactiveProxyError

ReactiveProxyError

Error message

Failed to transact text! yText is not attached to a doc

What it means

Every mutating Text method (insert, delete, format, replace, clear, split, join) funnels through `_transact`, which requires `yText.doc`. The Text constructor deliberately accepts unattached input — `new Text('hi')` creates a standalone `Y.Text` — so calling any mutation before that Y.Text is integrated into a Y.Doc throws this error.

Solutions

  1. Attach before mutating: `const t = new Text('hi'); ydoc.getMap('texts').set('t', t.yText); t.insert('!', 2);`
  2. Or construct from an already-attached Y.Text: `new Text(ydoc.getText('t'))`
  3. Check `text.yText.doc !== null` before calling any mutation method
  4. For never-synced scratch strings, keep a plain string and build the Text after attachment

Example fix

// before
const title = new Text('draft');
title.insert('!', 6); // throws: yText has no doc

// after
const title = new Text('draft');
ydoc.getMap('blocks').set('title', title.yText); // integrate
title.insert('!', 6);
Defensive patterns

Strategy: validation

Validate before calling

if (text.yText.doc === null) {
  ydoc.getMap('texts').set('t', text.yText); // integrate before editing
}
text.insert('!', text.length);

Type guard

const isEditableText = (t: Text): boolean => t.yText.doc !== null;

Try / catch

try {
  text.insert(content, index);
} catch (e) {
  if (e instanceof BlockSuiteError && e.code === ErrorCode.ReactiveProxyError && e.message.includes('not attached')) {
    ydoc.getMap('texts').set('t', text.yText);
    text.insert(content, index); // retry once attached
  } else throw e;
}

Prevention

When it happens

Trigger: `const t = new Text('hello'); t.insert('!', 5);`; `new Text(deltaArray).format(0, 1, {bold: true})`; `new Text(detachedYText).delete(0, 1)`; any mutation after the owning doc is destroyed.

Common situations: Building a Text from a plain string or delta for later attachment, then editing it too early; block-level text created before the block model is mounted in the doc; post-teardown editing callbacks.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/e093b460ad6084b6. Report an issue: GitHub.

Appendix: source

Thrown at blocksuite/framework/store/src/reactive/text/text.ts:99

    this._deltas$ = signal(this._yText.doc ? this._yText.toDelta() : []);
    this._yText.observe(event => {
      const isLocal =
        !event.transaction.origin ||
        !this._yText.doc ||
        event.transaction.origin instanceof Y.UndoManager ||
        event.transaction.origin.proxy
          ? true
          : event.transaction.origin === this._yText.doc.clientID;
      this._length$.value = this._yText.length;
      this._deltas$.value = this._yText.toDelta();
      this._onChange?.(this._yText, isLocal);
    });
  }

  private _transact(callback: () => void) {
    const doc = this._yText.doc;
    if (!doc) {
      throw new BlockSuiteError(
        ErrorCode.ReactiveProxyError,
        'Failed to transact text! yText is not attached to a doc'
      );
    }
    doc.transact(() => {
      callback();
    }, doc.clientID);
  }

  /**
   * Apply a delta to the text.
   *
   * @param delta - The delta to apply.
   *
   * @example
   * ```ts
   * const text = new Text('Hello, world!');
   * text.applyDelta([{insert: ' blocksuite', attributes: { bold: true }}]);

View on GitHub (pinned to b4c8548c09)