{"record":{"id":"1a4c8e18262b2db6","repo":"BookStackApp/BookStack","slug":"cannot-call-delete-on-a-frozen-lexical-node-map","errorCode":null,"errorMessage":"Cannot call delete() on a frozen Lexical node map","messagePattern":"Cannot call delete\\(\\) on a frozen Lexical node map","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"resources/js/wysiwyg/lexical/core/LexicalUpdates.ts","lineNumber":473,"sourceCode":"}\n\nfunction handleDEVOnlyPendingUpdateGuarantees(\n  pendingEditorState: EditorState,\n): void {\n  // Given we can't Object.freeze the nodeMap as it's a Map,\n  // we instead replace its set, clear and delete methods.\n  const nodeMap = pendingEditorState._nodeMap;\n\n  nodeMap.set = () => {\n    throw new Error('Cannot call set() on a frozen Lexical node map');\n  };\n\n  nodeMap.clear = () => {\n    throw new Error('Cannot call clear() on a frozen Lexical node map');\n  };\n\n  nodeMap.delete = () => {\n    throw new Error('Cannot call delete() on a frozen Lexical node map');\n  };\n}\n\nexport function $commitPendingUpdates(\n  editor: LexicalEditor,\n  recoveryEditorState?: EditorState,\n): void {\n  const pendingEditorState = editor._pendingEditorState;\n  const rootElement = editor._rootElement;\n  const shouldSkipDOM = editor._headless || rootElement === null;\n\n  if (pendingEditorState === null) {\n    return;\n  }\n\n  // ======\n  // Reconciliation has started.\n  // ======","sourceCodeStart":455,"sourceCodeEnd":491,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/resources/js/wysiwyg/lexical/core/LexicalUpdates.ts#L455-L491","documentation":"The third frozen node-map stub: dev builds throw 'Cannot call delete() on a frozen Lexical node map' when .delete() is invoked on the pending editor state's node map. Lexical freezes set/clear/delete to guarantee that pending/committed editor states are never mutated after creation.","triggerScenarios":"Dev-mode code calling nodeMap.delete(key) on a frozen pendingEditorState._nodeMap, typically from code holding a reference captured before $commitPendingUpdates froze the map.","commonSituations":"Plugins attempting to remove nodes by deleting map entries directly; custom undo/redo or state-pruning logic touching internals; dev-only (absent in production).","solutions":["Delete/remove nodes via editor.update() and $getNodeByKey(...).remove(), never by deleting map entries","Treat EditorState and its _nodeMap as strictly read-only","Audit and remove direct _nodeMap access from plugins and utilities","Report to Lexical if the throw originates from core/plugin code you don't control"],"exampleFix":"// before\neditorState._nodeMap.delete(key);\n// after\neditor.update(() => {\n  $getNodeByKey(key)?.remove();\n});","handlingStrategy":"try-catch","validationCode":"// Remove nodes through the editor, not the map\neditor.update(() => {\n  const node = $getNodeByKey(key);\n  if (node) node.remove();\n});","typeGuard":null,"tryCatchPattern":"try {\n  removeNodeByKey(editor, key);\n} catch (e) {\n  if (String(e.message).includes('frozen Lexical node map')) {\n    console.error('Use editor.update + node.remove(), not _nodeMap.delete');\n  }\n  throw e;\n}","preventionTips":["Delete nodes via $getNodeByKey(key).remove() inside editor.update()","Never manipulate EditorState._nodeMap directly","Keep plugin code free of Lexical internals","Catch and log dev-only invariant throws with stack traces to find offenders"],"tags":["javascript","lexical","rich-text","dev-only","immutability"],"backgroundTag":"lexical-immutable-state-violation","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}