toeverything/AFFiNE · error · BlockSuiteError

ReactiveProxyError

ReactiveProxyError

Error message

key cannot be a symbol

What it means

ReactiveYArray wraps a plain JS array and forwards writes into a Yjs Y.Array inside a doc transaction. Yjs keys are string-addressable only, so the proxy's `set` trap hard-rejects any write whose property key is a symbol before any data is touched. This is an invariant guard, not a data error: nothing is partially written when it fires.

Solutions

  1. Store symbol-keyed metadata in a side WeakMap keyed by the proxy instead of on the proxy itself
  2. Use plain string keys for any property you intentionally attach to the array
  3. Hand third-party code a plain copy (`[...arrProxy]` / `structuredClone(toJSON(arrProxy))`) so it stamps symbols on the copy, not the reactive proxy
  4. If the writer cannot be controlled, catch BlockSuiteError with code ReactiveProxyError and fall back to non-reactive storage

Example fix

// before
yArrayProxy[Symbol('meta')] = { touchedAt: Date.now() };

// after
const meta = new WeakKeyMap... // use a side table
const meta = new WeakMap<object, unknown>();
meta.set(yArrayProxy, { touchedAt: Date.now() });
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof key !== 'string') {
  sideTable.set(arrProxy, value); // never write symbol keys to the proxy
} else {
  arrProxy[key] = value;
}

Type guard

const isStringKey = (key: PropertyKey): key is string => typeof key === 'string';

Try / catch

try {
  arrProxy[key] = value;
} catch (e) {
  if (e instanceof BlockSuiteError && e.code === ErrorCode.ReactiveProxyError && e.message.includes('symbol')) {
    sideTable.set(arrProxy, value); // graceful fallback
  } else throw e;
}

Prevention

When it happens

Trigger: `arrProxy[Symbol('meta')] = value`; `Object.assign(reactiveArray, objWithSymbolKeys)`; a dependency (Vue reactivity flags, immer, class instances carrying symbol internals) stamping symbol-keyed metadata onto the reactive array; overriding `arrProxy[Symbol.iterator]`.

Common situations: Passing a BlockSuite reactive array into a reactivity/serialization library that decorates objects with symbol keys; using symbol keys as collision-free metadata on shared state; HMR setups that reuse proxies with framework internals that expect symbols.

Related errors


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

Appendix: source

Thrown at blocksuite/framework/store/src/reactive/proxy.ts:52

          this._updateWithSkip(() => {
            this._source.splice(retain, 0, ...proxyList);
          });

          retain += change.insert.length;
        }
      });
    });
  };

  protected _getProxy = () => {
    return new Proxy(this._source, {
      has: (target, p) => {
        return Reflect.has(target, p);
      },
      set: (target, p, value, receiver) => {
        if (typeof p !== 'string') {
          throw new BlockSuiteError(
            ErrorCode.ReactiveProxyError,
            'key cannot be a symbol'
          );
        }

        const index = Number(p);
        if (this._skipNext || Number.isNaN(index)) {
          return Reflect.set(target, p, value, receiver);
        }

        if (this._stashed.has(index)) {
          const result = Reflect.set(target, p, value, receiver);
          this._options.onChange?.(this._proxy, true);
          return result;
        }

        const reactive = proxies.get(this._ySource);
        if (!reactive) {

View on GitHub (pinned to b4c8548c09)