toeverything/AFFiNE · error · BlockSuiteError
ModelCRUDError
ModelCRUDError
Error message
updating block: ${model.id} not found What it means
Thrown by Store.updateBlock after the model was resolved from the block index but the underlying yBlock is missing from the Yjs map (_yBlocks.get(model.id) is undefined). This indicates the in-memory model and the yjs source of truth have diverged: the block was removed from the yMap (e.g. by a peer's delete replicated mid-call) while the local block index still held the model.
Source
Thrown at blocksuite/framework/store/src/model/store/store.ts:893
if (!model) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
`updating block: ${modelOrId} not found`
);
}
if (!isCallback) {
const parent = this.getParent(model);
this.schema.validate(
model.flavour,
parent?.flavour,
callBackOrProps.children?.map(child => child.flavour)
);
}
const yBlock = this._yBlocks.get(model.id);
if (!yBlock) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
`updating block: ${model.id} not found`
);
}
const block = this.getBlock(model.id);
if (!block) return;
this.transact(() => {
if (isCallback) {
callBackOrProps();
this._runQuery(block);
return;
}
if (callBackOrProps.children) {
this._crud.updateBlockChildren(
model.id,View on GitHub (pinned to 26c515e050)
Solutions
- Re-check existence immediately before mutating: if (!store._yBlocks.has(model.id)) return; or use the block index freshly.
- Sequence dependent operations so a delete aborts subsequent updates on the same block.
- Subscribe to yMap observe events and invalidate cached model references on delete.
- Wrap collaborative-edit handlers so they re-resolve the target after each await/transaction.
Example fix
// before store.updateBlock(model, props); // after if (!store.getBlock(model.id)) return; // model already gone store.updateBlock(model, props);
Defensive patterns
Strategy: validation
Validate before calling
export function assertYBlockExists(store: Store, model: BlockModel): void {
// Re-resolve right before mutating; the block index is the source of truth
if (!store.getBlock(model.id)) {
throw new Error(`updateBlock aborted: '${model.id}' no longer exists`);
}
}
assertYBlockExists(store, model);
store.updateBlock(model, props); Type guard
export const isLiveBlock = (store: Store, model: BlockModel): boolean => Boolean(store.getBlock(model.id));
Try / catch
try {
store.updateBlock(model, props);
} catch (e) {
if (e instanceof BlockSuiteError && e.code === ErrorCode.ModelCRUDError && /not found/.test(e.message)) {
// remote peer deleted the block mid-edit; abort this edit
return;
} else throw e;
} Prevention
- Re-check the block index immediately before updateBlock in collaborative flows.
- Subscribe to yMap observe events to invalidate stale model references on delete.
- Sequence dependent ops so a delete cancels subsequent updates on the same block.
- Re-resolve targets after each await/transaction in edit handlers.
When it happens
Trigger: Calling updateBlock on a model that was just deleted by a concurrent/remote operation; a delete transaction that removed the yBlock but the model index hasn't caught up; calling updateBlock inside a sequence where an earlier step deleted the block.
Common situations: Collaborative editing race (peer deletes while local user edits); undo/redo that removed the block; batched operations that delete-then-update; a stale model captured before a sync cycle.
Related errors
- ErrorCode.ModelCRUDError
- ErrorCode.ModelCRUDError
- ErrorCode.ModelCRUDError
- ErrorCode.ModelCRUDError
- ReactiveProxyError
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/d38051126fee60ec.
Report an issue: GitHub.