Summary
createToggleWrapper.ts calls editor.getBlock(block)! (non-null assertion) in four places. When getBlock returns undefined — a stale closed-over block whose node is no longer in the document — the very next thing that runs is `toggle-${block.id}` inside defaultToggledState.set, which throws.
Because these run inside raw DOM addEventListener handlers, the error is uncaught: it escapes React error boundaries and lands in window.onerror.
This is the same class of bug as #2847 / #2821 (TableHandles.ts), but in a different file, and it is not fixed by that change.
Version
- Reproduced in production on
@blocknote/core 0.49.0 and 0.52.1.
packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts is byte-identical between v0.49.0 and v0.52.1.
- Still present on
main at the time of writing (4 unguarded getBlock(block)! calls).
Where
https://github.com/TypeCellOS/BlockNote/blob/v0.52.1/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts
// defaultToggledState.set — line 12
set: (block, isToggled: boolean) =>
window.localStorage.setItem(
`toggle-${block.id}`, // <-- throws if `block` is undefined
isToggled ? "true" : "false",
),
Unguarded call sites: lines 55, 62, 114, 127. For example, in toggleButtonOnClick:
toggleWrapper.setAttribute("data-show-children", "true");
toggledState.set(editor.getBlock(block)!, true); // line 62 — unguarded
if (
editor.isEditable &&
editor.getBlock(block)?.children.length === 0 && // line 66 — guarded
!dom.contains(toggleAddBlockButton)
) {
Note the inconsistency: within the same function, getBlock(block) is optional-chained on line 66 and 108 but non-null-asserted on lines 55, 62, 114 and 127. That asymmetry suggests the ! is an oversight rather than an invariant the code can rely on.
Symptom
Uncaught TypeError, wording depending on the engine:
- Chrome/V8 —
Cannot read properties of undefined (reading 'id')
- Firefox/SpiderMonkey —
can't access property "id", e is undefined
Sentry reports it with mechanism.type: auto.browser.browserapierrors.addEventListener and handled: false. The breadcrumb immediately before the throw is a click on div.bn-toggle-wrapper > button.bn-toggle-button.
Reproduction
I have not found reliable manual steps — same situation as the reporter of #2847, who also found their case through production error monitoring rather than a manual repro.
What the data shows: it fires on documents containing toggle blocks, on both Chrome and Firefox, clustered around interactions with the toggle button and the block menus. The plausible path is that block is captured in the closure when the wrapper is created, the underlying node is later replaced or removed (a replaceBlocks, a collaborative/programmatic update, or a menu action that recreates the block), and a subsequent click on the still-mounted toggle button resolves getBlock to undefined.
Editor config, in case it helps narrow it down: schema with toggleListItem enabled alongside paragraph, heading, bulletListItem, numberedListItem, checkListItem, quote, codeBlock, image and table; @blocknote/mantine; no collaboration/Yjs. Content is loaded with replaceBlocks after parsing HTML or Markdown.
Impact for us is low — the editor keeps working and autosave still succeeds — but the errors are unhandled, so they surface as crashes in error reporting and are indistinguishable from real ones.
Suggested fix
Mirror what #2821 did for the table handles: bail out instead of asserting.
const currentBlock = editor.getBlock(block);
if (currentBlock) {
toggledState.set(currentBlock, false);
}
Making ToggledState["set"]/["get"] tolerate undefined would also work, but guarding at the call sites keeps the contract clearer and matches the existing optional chaining a few lines below.
Happy to open a PR if that would help.
Summary
createToggleWrapper.tscallseditor.getBlock(block)!(non-null assertion) in four places. WhengetBlockreturnsundefined— a stale closed-overblockwhose node is no longer in the document — the very next thing that runs is`toggle-${block.id}`insidedefaultToggledState.set, which throws.Because these run inside raw DOM
addEventListenerhandlers, the error is uncaught: it escapes React error boundaries and lands inwindow.onerror.This is the same class of bug as #2847 / #2821 (
TableHandles.ts), but in a different file, and it is not fixed by that change.Version
@blocknote/core0.49.0 and 0.52.1.packages/core/src/blocks/ToggleWrapper/createToggleWrapper.tsis byte-identical betweenv0.49.0andv0.52.1.mainat the time of writing (4 unguardedgetBlock(block)!calls).Where
https://github.com/TypeCellOS/BlockNote/blob/v0.52.1/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts
Unguarded call sites: lines 55, 62, 114, 127. For example, in
toggleButtonOnClick:Note the inconsistency: within the same function,
getBlock(block)is optional-chained on line 66 and 108 but non-null-asserted on lines 55, 62, 114 and 127. That asymmetry suggests the!is an oversight rather than an invariant the code can rely on.Symptom
Uncaught
TypeError, wording depending on the engine:Cannot read properties of undefined (reading 'id')can't access property "id", e is undefinedSentry reports it with
mechanism.type: auto.browser.browserapierrors.addEventListenerandhandled: false. The breadcrumb immediately before the throw is a click ondiv.bn-toggle-wrapper > button.bn-toggle-button.Reproduction
I have not found reliable manual steps — same situation as the reporter of #2847, who also found their case through production error monitoring rather than a manual repro.
What the data shows: it fires on documents containing toggle blocks, on both Chrome and Firefox, clustered around interactions with the toggle button and the block menus. The plausible path is that
blockis captured in the closure when the wrapper is created, the underlying node is later replaced or removed (areplaceBlocks, a collaborative/programmatic update, or a menu action that recreates the block), and a subsequent click on the still-mounted toggle button resolvesgetBlocktoundefined.Editor config, in case it helps narrow it down: schema with
toggleListItemenabled alongsideparagraph,heading,bulletListItem,numberedListItem,checkListItem,quote,codeBlock,imageandtable;@blocknote/mantine; no collaboration/Yjs. Content is loaded withreplaceBlocksafter parsing HTML or Markdown.Impact for us is low — the editor keeps working and autosave still succeeds — but the errors are unhandled, so they surface as crashes in error reporting and are indistinguishable from real ones.
Suggested fix
Mirror what #2821 did for the table handles: bail out instead of asserting.
Making
ToggledState["set"]/["get"]tolerateundefinedwould also work, but guarding at the call sites keeps the contract clearer and matches the existing optional chaining a few lines below.Happy to open a PR if that would help.