Skip to content

[Fizz] Bug: Nested suspense boundary with suspended fallback: "A previously unvisited boundary must have exactly one root segment" #36003

@switz

Description

@switz

There's a bug in a nested Suspense where the inner fallback itself suspends (via use() on a delay promise), and the inner content resolves before the fallback. Fizz mis-tracks boundary segments during the transition from "pending with suspended fallback" to "completed."

This is a useful pattern for when you want to create a DeferredSuspense boundary that blocks for a pre-set amount of time and then flushes. If the period passes and the children have not un-suspended, we flush the alternate boundary to the client.

I am aware this a cursed use of suspense, but it should be valid imo.

React version: 19.2.4

Steps To Reproduce

  1. $ node nested-suspense-bug-repro.mjs

Link to code example:

https://gist.github.com/switz/45d7c9f49953e11de7eec9d737841e2e#file-nested-suspense-bug-repro-mjs

The current behavior

$ node nested-suspense-bug-repro.mjs
onError: A previously unvisited boundary must have exactly one root segment. This is a bug in React.
Bug triggered: A previously unvisited boundary must have exactly one root segment. This is a bug in React.

The expected behavior

When content resolves FAST (before ms):

  • No fallback is ever shown — content renders directly

When content resolves SLOW (after ms):

  • For the first ms milliseconds, nothing visible changes (outer Suspense holds)
  • After ms, the fallback appears (inner Suspense fallback delay resolves, outer Suspense can now show its
    content which includes the pending inner boundary with its fallback)
  • When content finally resolves, it replaces the fallback via streaming

The basic mechanics:

  <Suspense fallback={fallback}>              ← outer: catches if inner fallback suspends
    <Suspense fallback={<Delay ms={200}>{fallback}</Delay>}>  ← inner: delay on fallback
      {children}                               ← the actual async content
    </Suspense>
  </Suspense>
  1. Children suspend → inner Suspense tries its fallback
  2. Fallback has which also suspends → outer Suspense catches both
  3. If children resolve before 200ms → inner boundary completes, outer shows content directly (no fallback
    flash)
  4. If 200ms elapses first → delay resolves, outer can now render (shows inner's fallback), children
    stream in later

The bug is in case 3 — Fizz crashes instead of rendering the content directly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Status: UnconfirmedA potential issue that we haven't yet confirmed as a bug

    Type

    No type
    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