BookStackApp/BookStack · error · Error

Cannot call set() on a frozen Lexical node map

Error message

Cannot call set() on a frozen Lexical node map

What it means

In dev builds, Lexical freezes the pending editor state's _nodeMap by replacing its set/clear/delete methods with throwing stubs (handleDEVOnlyPendingUpdateGuarantees). Any code that mutates the committed/pending node map after freezing indicates code is holding and mutating a stale EditorState's map, which Lexical forbids, so it throws 'Cannot call set() on a frozen Lexical node map'.

Source

Thrown at resources/js/wysiwyg/lexical/core/LexicalUpdates.ts:465

  try {
    return callbackFn();
  } finally {
    activeEditorState = previousActiveEditorState;
    isReadOnlyMode = previousReadOnlyMode;
    activeEditor = previousActiveEditor;
  }
}

function handleDEVOnlyPendingUpdateGuarantees(
  pendingEditorState: EditorState,
): void {
  // Given we can't Object.freeze the nodeMap as it's a Map,
  // we instead replace its set, clear and delete methods.
  const nodeMap = pendingEditorState._nodeMap;

  nodeMap.set = () => {
    throw new Error('Cannot call set() on a frozen Lexical node map');
  };

  nodeMap.clear = () => {
    throw new Error('Cannot call clear() on a frozen Lexical node map');
  };

  nodeMap.delete = () => {
    throw new Error('Cannot call delete() on a frozen Lexical node map');
  };
}

export function $commitPendingUpdates(
  editor: LexicalEditor,
  recoveryEditorState?: EditorState,
): void {
  const pendingEditorState = editor._pendingEditorState;
  const rootElement = editor._rootElement;
  const shouldSkipDOM = editor._headless || rootElement === null;

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Stop mutating EditorState._nodeMap; treat editor states as immutable snapshots
  2. Clone/derive state via official APIs (editor.parseEditorState, $nodes helpers) instead of touching internals
  3. Remove/rework code storing references to old editor states and calling map methods
  4. Report the offending plugin/library code that reaches into Lexical internals

Example fix

// before
const map = editorState._nodeMap;
map.set(key, node);
// after
editor.update(() => {
  const node = $getNodeByKey(key);
  // mutate via public APIs
});
Defensive patterns

Strategy: type-guard

Validate before calling

// Don't touch internals; guard before mutating any captured state
function isFrozenNodeMap(map: unknown): boolean {
  try { (map as Map<string, unknown>).set('__probe__', null); return false; }
  catch { return true; }
}

Type guard

function isLiveEditorState(state: EditorState, editor: LexicalEditor): boolean {
  return editor.getEditorState() === state;
}

Try / catch

try {
  // code that may hold stale state references
} catch (e) {
  if (e.message.includes('frozen Lexical node map')) {
    console.error('Mutating a committed EditorState is forbidden');
  }
  throw e;
}

Prevention

When it happens

Trigger: parseEditorState or $commitPendingUpdates later triggering a .set() call on the frozen map — e.g. custom code or plugin reading editorState._nodeMap and calling set() on it, or misusing internal APIs.

Common situations: Plugins reaching into private _nodeMap internals; custom editor-state serialization/deserialization attempts; dev-only — never occurs in production builds.

Related errors


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/0c0fec27844e970c. Report an issue: GitHub.