Skip to content

Renderer: commit-phase insertBefore NotFoundError from ElementErrorBoundary re-presenting stale children (+ index-keyed children) #727

Description

@illiachumak

Summary

@openuidev/react-lang@0.2.1's Renderer can crash the host app with a React commit-phase error while re-rendering or streaming content:

NotFoundError: Failed to execute 'insertBefore' on 'Node':
The node before which the new node is to be inserted is not a child of this node.

The error is thrown deep in React's commit (mutation) phase, so it cannot be caught by a nested error boundary in the host app — it escalates to the app root and unmounts the whole subtree. We hit it in production in a streaming chat UI, and also when a sibling re-render forces an already-rendered Renderer subtree to re-render.

Root cause

Two interacting behaviors in the renderer (line refs from the published dist/index.mjs, v0.2.1):

1. ElementErrorBoundary re-presents stale children on error. On hasError, render() returns this.lastValidChildren — React element objects captured from a previous render (componentDidMount/componentDidUpdate stash this.props.children). Those elements correspond to DOM nodes React has already reconciled/moved. Re-inserting them makes React's fiber tree disagree with the live DOM:

// dist/index.mjs ~L443-471
componentDidUpdate(prevProps) {
  if (!this.state.hasError) this.lastValidChildren = this.props.children;
  if (this.state.hasError && prevProps.children !== this.props.children) this.setState({ hasError: false });
}
render() {
  if (this.state.hasError) return this.lastValidChildren; // <-- stale element tree
  return this.props.children;
}

2. Array children are keyed by array index. renderDeep renders arrays as index-keyed fragments:

// dist/index.mjs ~L482
if (Array.isArray(value)) return value.map((v, i) => jsx(Fragment, { children: renderDeep(v) }, i));

When the parsed structure at a given index changes type between renders (which happens constantly while streaming a partial DSL, or when a child throws and the boundary swaps in stale children and back), index keys make React reconcile mismatched subtrees. Combined with (1), React's DOM bookkeeping desyncs and insertBefore(newNode, referenceNode) fails because referenceNode is no longer a child of the expected parent.

Reproduction (essence)

  1. Render a moderately deep tree (e.g. a Card with an array of child elements) via <Renderer>.
  2. Cause a child component to throw intermittently during re-render — e.g. stream a DSL so it is repeatedly re-parsed while partially formed, so a component occasionally receives malformed/partial props and throws.
  3. ElementErrorBoundary catches, returns stale lastValidChildren, the next valid render swaps back — at index-keyed positions whose types shift.
  4. Intermittently: NotFoundError: Failed to execute 'insertBefore' ... in React's commit phase, which escapes to the app root.

Suggested fixes

  • ElementErrorBoundary on error should NOT return stale element instances. Return null (or a caller-supplied fallback), or reset via a key bump so the subtree cleanly remounts. Re-inserting elements whose DOM React already moved is the core hazard.
  • Prefer stable keys over array index in renderDeep where a stable identity is derivable from the node, so type changes at a position remount cleanly rather than reconciling mismatched subtrees.

Workaround we're using

A local pnpm patch changing ElementErrorBoundary.render() to return null on error. This eliminated the crash across streaming and re-render triggers, at the cost of losing the "show last good state" behavior for a single element for one frame (acceptable vs. an app crash).

Environment

  • @openuidev/react-lang 0.2.1
  • React 19.2, Next.js 16

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is needed

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions