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

  1. Re-check existence immediately before mutating: if (!store._yBlocks.has(model.id)) return; or use the block index freshly.
  2. Sequence dependent operations so a delete aborts subsequent updates on the same block.
  3. Subscribe to yMap observe events and invalidate cached model references on delete.
  4. 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

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


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