Skip to content

RangeError: Position N out of range — getBlockFromPos resolves an unvalidated getPos() during nodeView mount #2937

Description

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions