{"record":{"id":"711c49798e928710","repo":"facebook/lexical","slug":"updateeditorsync-an-editor-update-e-g-a-command","errorCode":null,"errorMessage":"updateEditorSync: an editor update (e.g. a command listener that mutates the editor) ran while a read-only context was on the stack. This most commonly happens when a command is dispatched from inside editor.read(). The update has been deferred to a fresh writable update so it still applies, but dispatching mutations from a read-only context is an anti-pattern — dispatch after editor.read() returns, or via queueMicrotask.","messagePattern":"updateEditorSync: an editor update \\(e\\.g\\. a command listener that mutates the editor\\) ran while a read-only context was on the stack\\. This most commonly happens when a command is dispatched from inside editor\\.read\\(\\)\\. The update has been deferred to a fresh writable update so it still applies, but dispatching mutations from a read-only context is an anti-pattern — dispatch after editor\\.read\\(\\) returns, or via queueMicrotask\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/lexical/src/LexicalUpdates.ts","lineNumber":1313,"sourceCode":" * within an update\n */\nexport function updateEditorSync(\n  editor: LexicalEditor,\n  updateFn: () => void,\n  options?: EditorUpdateOptions,\n): void {\n  if (activeEditor === editor && options === undefined) {\n    if (isCurrentlyReadOnlyMode()) {\n      // We are nominally \"inside an update\" for this editor, but the active\n      // context is read-only (e.g. a command dispatched from inside\n      // editor.read(), or a force-commit read on the stack). Running updateFn\n      // inline here would mutate the frozen active editor state and throw\n      // \"Cannot call set() on a frozen Lexical node map\" — an error that gets\n      // routed to editor._onError rather than rethrown, so the mutation is\n      // silently dropped. Route through $beginUpdate instead, which starts a\n      // fresh writable update, so the work actually applies.\n      if (__DEV__) {\n        console.warn(\n          `updateEditorSync: an editor update (e.g. a command listener that ` +\n            `mutates the editor) ran while a read-only context was on the ` +\n            `stack. This most commonly happens when a command is dispatched ` +\n            `from inside editor.read(). The update has been deferred to a ` +\n            `fresh writable update so it still applies, but dispatching ` +\n            `mutations from a read-only context is an anti-pattern — dispatch ` +\n            `after editor.read() returns, or via queueMicrotask.`,\n        );\n      }\n      $beginUpdate(editor, updateFn, options);\n    } else {\n      updateFn();\n    }\n  } else {\n    $beginUpdate(editor, updateFn, options);\n  }\n}\n","sourceCodeStart":1295,"sourceCodeEnd":1331,"githubUrl":"https://github.com/facebook/lexical/blob/76a22dcba981b46119fcec45f935270707e26a57/packages/lexical/src/LexicalUpdates.ts#L1295-L1331","documentation":"updateEditorSync detected that an editor mutation (typically a command listener that mutates the editor) ran while a read-only context (editor.read()) was on the stack. If inlined directly it would mutate a frozen editor state and throw 'Cannot call set() on a frozen Lexical node map', which gets routed to editor._onError and silently dropped. Lexical instead defers the work through $beginUpdate into a fresh writable update so it still applies, and warns that dispatching mutations from a read-only context is an anti-pattern.","triggerScenarios":"Calling editor.dispatchCommand (or otherwise triggering updateEditorSync) from inside editor.read(...) where a registered command listener mutates the editor; any update attempt while a read-only context is active on the update stack.","commonSituations":"Dispatching a command inside an editor.read() block (e.g. reading state then immediately dispatching a formatting command); event handlers that call read() and then update synchronously; click/selection handlers performing read-then-dispatch.","solutions":["Move the dispatchCommand/update call outside the editor.read() callback — read first, then dispatch after read returns.","If asynchrony is acceptable, wrap the dispatch in queueMicrotask(() => editor.dispatchCommand(...)) so it runs after the read context unwinds.","Restructure the code so mutations use editor.update(() => ...) instead of mixing read and dispatch.","Use editor.read('pending', ...) or read('latest', ...) semantics appropriately rather than dispatching mid-read."],"exampleFix":"// before\neditor.read(() => {\n  if (needFix()) {\n    editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');\n  }\n});\n// after\neditor.read(() => {\n  if (needFix()) {\n    queueMicrotask(() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold'));\n  }\n});","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  editor.read(() => {\n    // read-only work only; dispatch AFTER read returns\n  });\n  if (shouldDispatch) editor.dispatchCommand(MY_COMMAND, payload);\n} catch (e) {\n  editor._onError?.(e);\n}","preventionTips":["Never dispatch commands or call editor.update() inside editor.read() callbacks.","Use queueMicrotask/setTimeout to defer dispatches triggered from read paths.","Keep read functions pure: return data, let the caller decide on mutations.","Listen for this dev warning in tests to catch read/dispatch mixing early."],"tags":["console-warning","read-only-context","update-cycle","command-dispatch","anti-pattern"],"backgroundTag":"update-during-read","analyzedSha":"76a22dcba981b46119fcec45f935270707e26a57","analyzedAt":"2026-08-31T21:45:54.792Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}