toeverything/AFFiNE · error · BlockSuiteError

ErrorCode.ModelCRUDError

ErrorCode.ModelCRUDError

Error message

cannot modify data in readonly mode

What it means

Thrown by Store.addBlock when this.readonly is true. A readonly store is a doc opened for read-only access (e.g. viewer mode, snapshot inspection); mutating it would desync viewers and could break collaboration guarantees, so every write API refuses up front.

Source

Thrown at blocksuite/framework/store/src/model/store/store.ts:750

  /**
   * Creates and adds a new block to the store
   * @param flavour - The block's flavour (type)
   * @param blockProps - Optional properties for the new block
   * @param parent - Optional parent block or parent block ID
   * @param parentIndex - Optional index position in parent's children
   * @returns The ID of the newly created block
   * @throws {BlockSuiteError} When store is in readonly mode
   *
   * @category Block CRUD
   */
  addBlock<T extends BlockModel = BlockModel>(
    flavour: string,
    blockProps: Partial<(PropsOfModel<T> & BlockSysProps) | BlockProps> = {},
    parent?: BlockModel | string | null,
    parentIndex?: number
  ): string {
    if (this.readonly) {
      throw new BlockSuiteError(
        ErrorCode.ModelCRUDError,
        'cannot modify data in readonly mode'
      );
    }

    const id = blockProps.id ?? this._doc.workspace.idGenerator();

    this.transact(() => {
      this._crud.addBlock(
        id,
        flavour,
        { ...blockProps },
        typeof parent === 'string' ? parent : parent?.id,
        parentIndex
      );
    });

    return id;

View on GitHub (pinned to 26c515e050)

Solutions

  1. Check store.readonly before issuing mutations and disable/skip the action in the UI.
  2. Open the doc in read-write mode if editing is intended: pass readonly:false when creating the session.
  3. Guard global edit handlers (keyboard, toolbar) with an if (store.readonly) return;.
  4. If readonly was set due to a transient condition (permissions, connection), re-open the doc writable once it clears.

Example fix

// before
store.addBlock('affine:paragraph', {}, parent);

// after
if (store.readonly) {
  notifyUser('Document is read-only');
  return;
}
store.addBlock('affine:paragraph', {}, parent);
Defensive patterns

Strategy: validation

Validate before calling

export function assertWritable(store: Store): void {
  if (store.readonly) {
    throw new Error('Store is read-only; mutation rejected');
  }
}

assertWritable(store);
store.addBlock('affine:paragraph', {}, parent);

Type guard

export const isWritable = (store: Store): boolean => !store.readonly;

Try / catch

try {
  store.addBlock(flavour, props, parent);
} catch (e) {
  if (e instanceof BlockSuiteError && e.code === ErrorCode.ModelCRUDError && /readonly mode/.test(e.message)) {
    notifyReadonlyMode(); // or re-open the doc writable
  } else throw e;
}

Prevention

When it happens

Trigger: Calling store.addBlock (or any mutating API routed through addBlock) on a doc/collection opened with readonly:true, or after the doc was switched to readonly at runtime.

Common situations: Embedding a doc in read-only viewer mode and accidentally firing an edit command; shared/preview links opened readonly but the UI still binds edit handlers; a doc auto-switched to readonly after losing connection or on sync errors.

Related errors


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