facebook/react
Suspense
A Suspense boundary catches asynchronous "I'm not ready" signals from its descendants and shows a fallback in their place until they are. The implementation is one of the most cross-cutting features in the runtime, touching the reconciler, the streaming server (Fizz), the Flight client, and the DevTools backend.
The core trick
Inside a render, a component can throw a thenable (throw promise) to signal "I'm not done; come back when this resolves." The reconciler's ReactFiberThrow.js distinguishes thenables from Errors: thenables propagate up to the nearest <Suspense> boundary, errors propagate up to the nearest error boundary.
graph TD Render[Render Foo] -->|throw promise| Throw[ReactFiberThrow.handleThrow] Throw -->|walk up tree| Suspense[Find nearest Suspense fiber] Suspense -->|register thenable| Retry[ReactFiberThenable: trackUsedThenable] Suspense -->|mark fiber DidCapture| Reconciler Reconciler -->|render fallback| Commit Promise[promise resolves] -->|wakeable.then| Retry Retry -->|markRootUpdated retry lane| RootScheduler RootScheduler -->|reschedule| Reconciler Reconciler -->|re-render Foo| Done[succeeds → unsuspend]
use(promise) (ReactFiberHooks.useThenable) is the canonical way to throw a thenable. Most libraries (TanStack Query, Apollo, Relay) wrap their fetches in use() so they participate in Suspense automatically.
Components and files
| Component | File |
|---|---|
<Suspense> |
exported from packages/react/src/ReactClient.js (just a symbol — REACT_SUSPENSE_TYPE); the runtime behavior is in the reconciler. |
| Suspense fiber tag | packages/react-reconciler/src/ReactWorkTags.js (SuspenseComponent). |
| Boundary state | packages/react-reconciler/src/ReactFiberSuspenseComponent.js |
| Boundary context | packages/react-reconciler/src/ReactFiberSuspenseContext.js |
| Throw handling | packages/react-reconciler/src/ReactFiberThrow.js |
| Thenable tracking | packages/react-reconciler/src/ReactFiberThenable.js |
| Hidden context (subtree visibility) | packages/react-reconciler/src/ReactFiberHiddenContext.js |
| SSR-side boundary handling | packages/react-server/src/ReactFizzServer.js |
| Flight-side boundary handling | packages/react-server/src/ReactFlightServer.js |
Retries
When a thrown thenable resolves, its .then(...) callback calls pingSuspendedRoot (in ReactFiberWorkLoop.js), which:
- Marks the boundary's lane as no longer suspended on the root (
markRootPinged). - Schedules a new render at a
RetryLanepriority — typically lower than user input but higher than idle. - The next tick re-renders the boundary;
use(promise)now returns synchronously and the render succeeds.
RetryLanes are a separate slice of the priority space because they shouldn't block higher-priority interactive updates but should still come before idle work.
Streaming SSR (Fizz) and Suspense
The streaming SSR engine in packages/react-server/src/ReactFizzServer.js is built around Suspense. Each boundary becomes one or more segments — chunks of HTML that may stream out of order:
sequenceDiagram participant App participant Fizz as ReactFizzServer participant Stream App->>Fizz: renderToReadableStream(<App/>) Fizz->>Stream: emit shell HTML Fizz->>Fizz: hit <Suspense> with thrown promise Fizz->>Stream: emit fallback inline (with placeholder id) Fizz->>Fizz: keep working on other branches Fizz->>Fizz: when promise resolves, render the boundary content Fizz->>Stream: emit completed boundary HTML in a <template> Fizz->>Stream: emit a <script> snippet that swaps fallback for content
The "tiny inline runtime" that does the swap is generated by scripts/rollup/generate-inline-fizz-runtime.js and inlined at compile time into Fizz output.
prerender and resume extend Fizz with a postpone() primitive — when a boundary postpones, Fizz emits a marker and stops; later, a different request can pick up where the prerender left off. This is the mechanism behind React 19.2's partial pre-rendering.
Selective hydration
On the client, hydration walks the existing DOM and matches it to a fresh React tree. With streaming SSR, boundaries arrive over time. packages/react-reconciler/src/ReactFiberHydrationContext.js orchestrates:
- Boundaries hydrate at low priority by default.
- A user click on a still-unhydrated boundary bumps its priority to
SyncHydrationLane. - Boundaries can resuspend during hydration (the SSR HTML mismatched the client tree); when they do,
ReactFiberHydrationDiffs.jsproduces the rich "Server: ... Client: ..." mismatch error.
SuspenseInstance (a host-config concept) is the host platform's representation of a not-yet-ready boundary. For the DOM, it's a comment node like <!--$?--> placed by Fizz; the reconciler walks past it during hydration.
Activity (<Activity>) and Suspense
<Activity mode="hidden"> is essentially "Suspense's evil twin": it hides a subtree and tears down its effects, but preserves state. Code in packages/react-reconciler/src/ReactFiberOffscreenComponent.js.
The two cooperate:
- A subtree inside an
<Activity mode="hidden">doesn't suspend the visible tree. - A revealed
<Activity>may need to flush previously-deferred work; the reconciler handles this viaOffscreenLane.
View transitions and Suspense
<ViewTransition> integrates with Suspense in two ways:
- A boundary that resolves during an active view transition has its content's reveal coordinated with the transition's animation phase.
- View transitions can themselves be gesture-driven, with the gesture scheduler (
ReactFiberGestureScheduler.js) coordinating with retry lanes.
The full integration is still landing — code in ReactFiberCommitViewTransitions.js and ReactFiberApplyGesture.js.
Where to start when a Suspense bug shows up
- Reproduce in
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js. Most Suspense bugs are diagnosable againstreact-noop-rendererrather thanreact-dom. - Add
Scheduler.log(...)calls inReactFiberThrow.handleThrowand the Suspense-related code paths inReactFiberBeginWork.jsandReactFiberCompleteWork.js. - For SSR/streaming bugs, reproduce in
fixtures/ssr2/or one of the Flight fixtures, and tail the byte stream.
Related pages
- features/hooks — how
use(promise)fits into the hook lifecycle. - features/server-components — Suspense as the load-coordination primitive in RSC.
- packages/react-reconciler — the engine.
- packages/react-server — Fizz and the Flight server.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.