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
- Do not assign symbol-keyed properties to reactive Yjs proxies; keep symbol metadata on a separate plain object.
- Before Object.assign/spread onto a reactive prop, strip symbol keys: filter Object.getOwnPropertySymbols out.
- If a library needs to tag the object, wrap the reactive proxy in a plain carrier object and tag the carrier.
- 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
- Never assign symbol-keyed properties onto reactive Yjs proxies.
- Filter Object.getOwnPropertySymbols before Object.assign/spread onto reactive props.
- Keep symbol metadata on a separate plain carrier object, not the proxy.
- Audit third-party middleware (React devtools, instrumentation) for symbol writes.
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.