Repository navigation
[lexical] Bug Fix: Explain how to guard a stale node reference in the missing node error - #9366
Merged
Merged
Conversation
… missing node error ## Description When a node reference is kept from an earlier read or update (in a closure, an event handler or React state) and the node is removed before the reference is used again, `getLatest()` and `getWritable()` throw: ``` Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editorState.read/editor.update. ``` The message doesn't say which node it was or how to avoid the error, and "avoid using the same node references" reads as if keeping a node reference were the mistake, while the docs recommend storing one. As discussed in meta#9111, the fix on the caller's side is to check `node.isAttached()`, or to look the node up with `$getNodeByKey()`, inside the read or update that uses the reference. This rewords the invariant in `getLatest()` and `getWritable()` (they share one message) so that it includes the node key and type and names those two guards. The `Lexical node does not exist in active editor state` prefix is unchanged, so existing searches and assertions still match. The JSDoc of both methods now says when they throw. `scripts/error-codes/codes.json` is not touched, so the new message gets its production code in the usual release step. The documentation part of meta#9111 is in meta#9268; this change only covers the error message. Related to meta#9111 ## Test plan ### Before New tests in `LexicalWritableNode.test.ts`, run against `main`: ``` $ npx vitest --project unit --no-watch packages/lexical/src/__tests__/unit/LexicalWritableNode.test.ts -t "stale node references" × explains how to guard a stale reference when reading 34ms × explains how to guard a stale reference when writing 6ms Received: "Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editorState.read/editor.update." Received: "Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editorState.read/editor.update." Test Files 1 failed (1) Tests 2 failed | 1 passed | 8 skipped (11) ``` ### After ``` $ npx vitest --project unit --no-watch packages/lexical/src/__tests__/unit/LexicalWritableNode.test.ts -t "stale node references" --reporter=verbose ✓ |unit| packages/lexical/src/__tests__/unit/LexicalWritableNode.test.ts > stale node references > explains how to guard a stale reference when reading 26ms ✓ |unit| packages/lexical/src/__tests__/unit/LexicalWritableNode.test.ts > stale node references > explains how to guard a stale reference when writing 3ms ✓ |unit| packages/lexical/src/__tests__/unit/LexicalWritableNode.test.ts > stale node references > the suggested guards do not throw for a stale reference 3ms Test Files 1 passed (1) Tests 3 passed | 8 skipped (11) ``` The thrown message is now: ``` Lexical node does not exist in active editor state (node 1, type text). The reference was probably kept from another read or update, for example in a closure, event handler or component state, and the node has since been removed or is not part of this editor state. Before using a node reference that may be stale, check node.isAttached() or look the node up again with $getNodeByKey(node.getKey()), inside the read or update where it is used. ``` ``` $ npx vitest --project unit --project scripts-unit --no-watch packages/lexical/ scripts/error-codes Tests 1 failed | 2201 passed (2202) ``` The one failure is `LexicalGeneratedJSON.test.ts` ("the checked-in outputs are what the generator produces"), which fails the same way on a clean `main` checkout on my machine, so it is unrelated. `tsc`, `flow full-check` (0 errors), and `eslint` and `prettier --check` on the changed files pass. All of this was run on Windows with Node 24.21.0; browser and e2e tests were not run since only the message text changed.
Thanatos9404
requested review from
acywatson,
etrepum,
fantactuka,
ivailop7,
potatowagon and
zurfyx
as code owners
October 18, 2026 17:31
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
etrepum
reviewed
Oct 18, 2026
| invariant( | ||
| latestNode !== undefined, | ||
| 'Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editorState.read/editor.update.', | ||
| 'Lexical node does not exist in active editor state (node %s, type %s). The reference was probably kept from another read or update, for example in a closure, event handler or component state, and the node has since been removed or is not part of this editor state. Before using a node reference that may be stale, check node.isAttached() or look the node up again with $getNodeByKey(node.getKey()), inside the read or update where it is used.', |
Collaborator
There was a problem hiding this comment.
Suggested change
| 'Lexical node does not exist in active editor state (node %s, type %s). The reference was probably kept from another read or update, for example in a closure, event handler or component state, and the node has since been removed or is not part of this editor state. Before using a node reference that may be stale, check node.isAttached() or look the node up again with $getNodeByKey(node.getKey()), inside the read or update where it is used.', | |
| 'Lexical node does not exist in active editor state (key %s, type %s). The reference was probably kept from another read or update, for example in a closure, event handler or component state, and the node has since been removed or is not part of this editor state. Before using a node reference that may be stale, check node.isAttached() or look the node up again with $getNodeByKey(node.getKey()), inside the read or update where it is used.', |
| invariant( | ||
| false, | ||
| 'Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editorState.read/editor.update.', | ||
| 'Lexical node does not exist in active editor state (node %s, type %s). The reference was probably kept from another read or update, for example in a closure, event handler or component state, and the node has since been removed or is not part of this editor state. Before using a node reference that may be stale, check node.isAttached() or look the node up again with $getNodeByKey(node.getKey()), inside the read or update where it is used.', |
Collaborator
There was a problem hiding this comment.
Suggested change
| 'Lexical node does not exist in active editor state (node %s, type %s). The reference was probably kept from another read or update, for example in a closure, event handler or component state, and the node has since been removed or is not part of this editor state. Before using a node reference that may be stale, check node.isAttached() or look the node up again with $getNodeByKey(node.getKey()), inside the read or update where it is used.', | |
| 'Lexical node does not exist in active editor state (key %s, type %s). The reference was probably kept from another read or update, for example in a closure, event handler or component state, and the node has since been removed or is not part of this editor state. Before using a node reference that may be stale, check node.isAttached() or look the node up again with $getNodeByKey(node.getKey()), inside the read or update where it is used.', |
Comment on lines
+1586
to
+1591
| * Throws if the node is not in the active EditorState, for example when a | ||
| * reference kept from an earlier read or update points to a node that has | ||
| * since been removed. Check {@link LexicalNode.isAttached} first, which | ||
| * does not throw for a removed node, or look the node up with | ||
| * {@link $getNodeByKey}. | ||
| * |
Collaborator
There was a problem hiding this comment.
I don't think that changing the API docs will provide much value because getLatest and getWritable are almost always called indirectly from getter and setter methods, not explicitly. The improvements to the exception should be sufficient, and the types already imply that it needs to throw if the node doesn't exist.
## Description
Uses the wording suggested in review for the missing node invariant in
getLatest() and getWritable() ("key %s" instead of "node %s"), and drops
the JSDoc additions, since these methods are almost always reached
through getters and setters rather than called directly.
Related to meta#9111
## Test plan
### Before
The message read "(node <key>, type <type>)".
### After
```
$ npx vitest --project unit --no-watch packages/lexical/src/__tests__/unit/LexicalWritableNode.test.ts packages/lexical/src/__tests__/unit/LexicalNode.test.ts
Test Files 2 passed (2)
Tests 146 passed (146)
```
Contributor
Author
etrepum
approved these changes
Oct 18, 2026
Merged
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
When a node reference is kept from an earlier read or update (in a closure,
an event handler or React state) and the node is removed before the
reference is used again,
getLatest()andgetWritable()throw:The message doesn't say which node it was or how to avoid the error, and
"avoid using the same node references" reads as if keeping a node reference
were the mistake, while the docs recommend storing one. As discussed in
#9111, the fix on the caller's side is to check
node.isAttached(), or tolook the node up with
$getNodeByKey(), inside the read or update that usesthe reference.
This rewords the invariant in
getLatest()andgetWritable()(they shareone message) so that it includes the node key and type and names those two
guards. The
Lexical node does not exist in active editor stateprefix isunchanged, so existing searches and assertions still match. The JSDoc of
both methods now says when they throw.
scripts/error-codes/codes.jsonisnot touched, so the new message gets its production code in the usual
release step.
The documentation part of #9111 is in #9268; this change only covers the
error message.
Related to #9111
Test plan
Before
New tests in
LexicalWritableNode.test.ts, run againstmain:After
The thrown message is now:
The one failure is
LexicalGeneratedJSON.test.ts("the checked-in outputsare what the generator produces"), which fails the same way on a clean
maincheckout on my machine, so it is unrelated.tsc,flow full-check(0 errors), and
eslintandprettier --checkon the changed files pass.All of this was run on Windows with Node 24.21.0; browser and e2e tests were
not run since only the message text changed.