Skip to content

[lexical] Bug Fix: Explain how to guard a stale node reference in the missing node error - #9366

Merged
etrepum merged 2 commits into
meta:mainfrom
Thanatos9404:docs/9111-stale-node-invariant
Oct 18, 2026
Merged

etrepum merged 2 commits into
meta:mainfrom
Thanatos9404:docs/9111-stale-node-invariant

Conversation

@Thanatos9404

Copy link
Copy Markdown
Contributor

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
#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 #9111 is in #9268; this change only covers the
error message.

Related to #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.

… 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.
@vercel

vercel Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lexical Ready Ready Preview Oct 18, 2026 6:23pm UTC
lexical-playground Ready Ready Preview Oct 18, 2026 6:23pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 18, 2026
Comment thread packages/lexical/src/LexicalNode.ts Outdated
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.',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 thread packages/lexical/src/LexicalNode.ts Outdated
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.',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 thread packages/lexical/src/LexicalNode.ts Outdated
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}.
*

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
```
@Thanatos9404

Copy link
Copy Markdown
Contributor Author

Thanks @etrepum, applied both suggestions and dropped the JSDoc changes in bac421e.

@etrepum
etrepum added this pull request to the merge queue Oct 18, 2026
Merged via the queue into meta:main with commit 60a2da2 Oct 18, 2026
48 checks passed
@zurfyx zurfyx mentioned this pull request Oct 27, 2026

This branch was successfully deployed

2 active deployments
Preview – lexical — bac421eb Deployed Oct 18, 2026 by vercel[bot]
Preview – lexical-playground — bac421eb Deployed Oct 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

2 participants