What's broken?
getBlockFromPos calls doc.resolve(pos) with no bounds check against doc.content.size, so when a node view is constructed with a getPos() that points past the end of the current document, ProseMirror throws a raw RangeError: Position N out of range out of the render path.
We hit this in production (RangeError: Position 2604 out of range). It reached our route-level React error boundary, so the user lost their editing session to a full-page error screen.
The relevant code, from the 0.45.0 build we run:
function getBlockFromPos(getPos, editor, view, type) {
const pos = getPos();
if (pos === undefined) throw new Error("Cannot find node position");
const id = view.state.doc.resolve(pos).node().attrs.id; // <-- unvalidated
if (!id) throw new Error("Block doesn't have id");
...
This is still unguarded on the current release. doc.resolve(pos) in 0.52.1 has no bounds check and no try/catch:
https://cdn.jsdelivr.net/npm/@blocknote/core@0.52.1/src/schema/blocks/internal.ts
Where it comes from
The stack shows the throw happening while ProseMirror is reconciling the doc, inside a React node view being mounted synchronously:
prosemirror ResolvedPos.resolve <- RangeError: Position 2604 out of range
<- ResolvedPos.resolveCached
<- getBlockFromPos
<- (React block-spec render)
<- ReactRenderer ctor -> flushSync(render)
<- ReactNodeViewRenderer.mount
<- NodeViewDesc.create
<- ViewTreeUpdater.addNode
<- iterDeco
<- NodeViewDesc.updateChildren (x several, nested)
So a node view is created during updateChildren, its React render runs synchronously via flushSync, and it reads a getPos() that still reflects the pre-change document. If the document shrank in that transaction, the position is now out of range and the resolve throws from inside render.
What did you expect to happen?
A stale position during mount should degrade rather than throw from the render path — e.g. bounds-check pos against doc.content.size and return undefined/null so the node view can skip the frame and re-render on the next transaction, or throw a typed BlockNote error that consumers can catch meaningfully.
Steps to reproduce
I don't have a minimal reproduction — this is a low-frequency production crash (a handful of occurrences), and I couldn't reduce it to a reliable local repro. What I can offer instead is the exact code path above and the observation that the guard is absent in current code, which is reproducible by inspection.
Filing anyway because the two closest existing reports were both closed for lack of a repro, and the underlying missing bounds check is still there:
Both suggest the family is real and hard to pin down. A defensive bounds check would turn all of them from a crash into a recoverable no-op regardless of which trigger produced the bad position.
BlockNote version
@blocknote/core, @blocknote/react, @blocknote/mantine, @blocknote/xl-multi-column 0.45.0 (the missing guard verified present in 0.52.1 as linked above).
Environment
Chrome 150 / macOS, production build. React 19.2.3, @tiptap/core 3.17.0. No Yjs / collaboration. Custom blocks via createReactBlockSpec, with multiple editor instances on a page.
What's broken?
getBlockFromPoscallsdoc.resolve(pos)with no bounds check againstdoc.content.size, so when a node view is constructed with agetPos()that points past the end of the current document, ProseMirror throws a rawRangeError: Position N out of rangeout of the render path.We hit this in production (
RangeError: Position 2604 out of range). It reached our route-level React error boundary, so the user lost their editing session to a full-page error screen.The relevant code, from the 0.45.0 build we run:
This is still unguarded on the current release.
doc.resolve(pos)in 0.52.1 has no bounds check and no try/catch:https://cdn.jsdelivr.net/npm/@blocknote/core@0.52.1/src/schema/blocks/internal.ts
Where it comes from
The stack shows the throw happening while ProseMirror is reconciling the doc, inside a React node view being mounted synchronously:
So a node view is created during
updateChildren, its React render runs synchronously viaflushSync, and it reads agetPos()that still reflects the pre-change document. If the document shrank in that transaction, the position is now out of range and theresolvethrows from inside render.What did you expect to happen?
A stale position during mount should degrade rather than throw from the render path — e.g. bounds-check
posagainstdoc.content.sizeand returnundefined/nullso the node view can skip the frame and re-render on the next transaction, or throw a typed BlockNote error that consumers can catch meaningfully.Steps to reproduce
I don't have a minimal reproduction — this is a low-frequency production crash (a handful of occurrences), and I couldn't reduce it to a reliable local repro. What I can offer instead is the exact code path above and the observation that the guard is absent in current code, which is reproducible by inspection.
Filing anyway because the two closest existing reports were both closed for lack of a repro, and the underlying missing bounds check is still there:
updateChildrenpath; closed as not reproducible.Both suggest the family is real and hard to pin down. A defensive bounds check would turn all of them from a crash into a recoverable no-op regardless of which trigger produced the bad position.
BlockNote version
@blocknote/core,@blocknote/react,@blocknote/mantine,@blocknote/xl-multi-column0.45.0 (the missing guard verified present in 0.52.1 as linked above).Environment
Chrome 150 / macOS, production build. React 19.2.3, @tiptap/core 3.17.0. No Yjs / collaboration. Custom blocks via
createReactBlockSpec, with multiple editor instances on a page.