toeverything/AFFiNE · error · BlockSuiteError

ReactiveProxyError

ReactiveProxyError

Error message

key cannot be a symbol

What it means

Thrown by the ReactiveYArray (and ReactiveYMap) Proxy `set`/`deleteProperty` traps when the property key `p` is not a string. The reactive layer translates every mutation into a Yjs transaction keyed by a string index/name; symbol keys have no Yjs representation, so they are rejected to prevent silent data loss (a symbol-keyed write would update the local proxy array but never sync).

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 26c515e050)

Solutions

  1. Do not assign symbol-keyed properties to reactive Yjs proxies; keep symbol metadata on a separate plain object.
  2. Before Object.assign/spread onto a reactive prop, strip symbol keys: filter Object.getOwnPropertySymbols out.
  3. If a library needs to tag the object, wrap the reactive proxy in a plain carrier object and tag the carrier.
  4. Audit third-party middleware that touches model.props for symbol writes.

Example fix

// before: assigning an object that carries symbol keys
Object.assign(block.props.style, maybeTaggedObj);

// after: strip symbols before writing into the reactive proxy
const clean = Object.fromEntries(
  Object.entries(maybeTaggedObj)
);
Object.assign(block.props.style, clean);
Defensive patterns

Strategy: type-guard

Validate before calling

export function stripSymbols<T extends object>(obj: T): T {
  for (const sym of Object.getOwnPropertySymbols(obj)) {
    delete (obj as Record<symbol, unknown>)[sym];
  }
  return obj;
}

// before assigning onto a reactive proxy
const clean = Object.fromEntries(Object.entries(maybeTagged));
Object.assign(block.props.style, clean);

Type guard

export const hasOnlyStringKeys = (obj: object): boolean =>
  Object.getOwnPropertySymbols(obj).length === 0;

Try / catch

try {
  Object.assign(reactiveProxy, value);
} catch (e) {
  if (e instanceof BlockSuiteError && e.code === ErrorCode.ReactiveProxyError && /symbol/.test(e.message)) {
    const clean = Object.fromEntries(Object.entries(value));
    Object.assign(reactiveProxy, clean);
  } else throw e;
}

Prevention

When it happens

Trigger: Assigning a symbol-keyed property on a reactive Yjs array/map proxy: arr[Symbol.iterator] = ..., or libraries that set symbol properties (e.g. some decorators, React internals, immer) on the proxied object; spreading/assigning objects with symbol keys onto a reactive prop.

Common situations: Third-party utilities (React devtools, profiling, instrumentation) tagging the proxied object with a symbol; user code doing Object.assign(reactiveProp, someObjWithSymbols); iterating with for-in vs for-of confusion; test doubles attaching metadata.

Related errors


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