Suspense & Error Boundaries
The reconciler handles a component that stops rendering by classifying the thrown value and routing it to a boundary that can respond. A promise-like value enters the Suspense path; another error enters the error-capture path.
This separation exists because loading and failure need different recovery behavior: Suspense can render a temporary fallback and retry later, while an error boundary captures the error and schedules state or root updates.
Sources: packages/react-reconciler/src/ReactFiberThrow.js:120-201, packages/react-reconciler/src/ReactFiberThrow.js:93-112
Core concepts
Wakeable
A wakeable is a thrown object with a callable then method, which the reconciler treats as a suspended render rather than an error.
Sources: packages/react-reconciler/src/ReactFiberThrow.js:381-385
Suspense boundary
A Suspense boundary is the SuspenseComponent, SuspenseListComponent, or related activity boundary selected to render a fallback when descendant work suspends.
Sources: packages/react-reconciler/src/ReactFiberThrow.js:396-404
Error boundary
An error boundary is a class fiber whose type or instance provides getDerivedStateFromError or componentDidCatch, allowing the reconciler to capture a child error and run recovery behavior.
Sources: packages/react-reconciler/src/ReactFiberThrow.js:120-201
Captured update
A captured update is an update tagged CaptureUpdate; it either renders null at the root or lets a class boundary apply its error-derived state.
Sources: packages/react-reconciler/src/ReactFiberThrow.js:93-112, packages/react-reconciler/src/ReactFiberThrow.js:114-118
How a thrown promise becomes a Suspense fallback
During render, the work loop passes the thrown value, the source fiber, and its parent fiber to throwException. The work loop uses the Boolean result to decide whether the error was fatal; a fatal result calls panicOnRootError.
The first Suspense-specific step marks the source fiber as incomplete and recognizes a wakeable by checking for value.then. The reconciler then calls resetSuspendedComponent, because the suspended component did not complete and its deferred children need context propagation before retrying.
throwException obtains the active Suspense handler and, for a Suspense-compatible boundary, calls markSuspenseBoundaryShouldCapture. That function marks the boundary so unwinding can capture the suspended exception and perform a second pass that renders the fallback.
The fallback is therefore a boundary render state, not an error message generated by the thrown promise. The boundary may delay committing or restart work according to shell and fallback heuristics, while the suspended tree remains eligible for a later retry.
if (value !== null && typeof value === 'object') {
if (typeof value.then === 'function') {
const wakeable: Wakeable = value as any;
resetSuspendedComponent(sourceFiber, rootRenderLanes);
const suspenseBoundary = getSuspenseHandler();
if (suspenseBoundary !== null) {
markSuspenseBoundaryShouldCapture(
suspenseBoundary,
returnFiber,
sourceFiber,
root,
rootRenderLanes,
);This excerpt shows the classification boundary: a thrown object with then is reset and routed to the active Suspense boundary instead of the error-update path.
A completed fallback needs a retry mechanism. The reconciler stores the wakeable on the boundary during the render-to-commit flow and attaches a listener so resolution can schedule another render that turns the fallback state off.
Evidence
- renderingpackages/react-reconciler/src/ReactFiberThrow.js:364
- incomplete-workpackages/react-reconciler/src/ReactFiberThrow.js:364
- suspense-boundarypackages/react-reconciler/src/ReactFiberThrow.js:396
- fallbackpackages/react-reconciler/src/ReactFiberThrow.js:248
- retrypackages/react-reconciler/src/ReactFiberThrow.js:364
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:3247-3260, packages/react-reconciler/src/ReactFiberThrow.js:364-385, packages/react-reconciler/src/ReactFiberThrow.js:203-217, packages/react-reconciler/src/ReactFiberThrow.js:396-445, packages/react-reconciler/src/ReactFiberThrow.js:248-259, packages/react-reconciler/src/ReactFiberThrow.js:413-435, packages/react-reconciler/src/ReactFiberThrow.js:316-335, packages/react-reconciler/src/ReactFiberThrow.js:447-456
How an error reaches an error boundary
If the thrown value is not recognized as a wakeable, throwException continues through the error path and can create a captured update for a class boundary. The work loop describes this operation as finding and marking the nearest Suspense or error boundary that can handle the exception.
For a class boundary, createClassErrorUpdate creates an update tagged CaptureUpdate. initializeClassErrorUpdate then inspects the boundary’s getDerivedStateFromError and instance’s componentDidCatch methods.
When getDerivedStateFromError exists, its return value becomes the update payload, so the boundary can render an error fallback from derived state. When componentDidCatch exists, its callback logs the caught error and invokes componentDidCatch with the error and component stack.
This is how a boundary several levels above the throwing child can recover: the child is identified as the incomplete source fiber, the work loop delegates exception handling to throwException, and the selected boundary receives a captured update rather than requiring the child to catch its own render error.
If no boundary handles the error, createRootErrorUpdate creates a CaptureUpdate whose payload renders null at the root and whose callback logs the uncaught error.
Evidence
- work-looppackages/react-reconciler/src/ReactFiberWorkLoop.js:3247
- throwing-childpackages/react-reconciler/src/ReactFiberThrow.js:364
- error-boundarypackages/react-reconciler/src/ReactFiberThrow.js:114
- boundary-updatepackages/react-reconciler/src/ReactFiberThrow.js:120
- rootpackages/react-reconciler/src/ReactFiberThrow.js:93
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:3247-3257, packages/react-reconciler/src/ReactFiberThrow.js:364-705, packages/react-reconciler/src/ReactFiberThrow.js:114-118, packages/react-reconciler/src/ReactFiberThrow.js:120-201, packages/react-reconciler/src/ReactFiberThrow.js:126-147, packages/react-reconciler/src/ReactFiberThrow.js:150-181, packages/react-reconciler/src/ReactFiberThrow.js:364-372, packages/react-reconciler/src/ReactFiberThrow.js:93-112
Suspense fallback versus error fallback
The two fallback types differ in both trigger and recovery path.
| Case | Trigger | Boundary operation | Recovery |
|---|---|---|---|
| Suspense fallback | A wakeable with then is thrown | Mark the Suspense boundary to capture and render a second fallback pass | Attach a retry listener and render again when the wakeable resolves |
| Error-boundary fallback | A non-wakeable error reaches a class boundary | Create CaptureUpdate and initialize it from getDerivedStateFromError or componentDidCatch | Apply derived state or invoke componentDidCatch |
| Root failure | No boundary handles the error | Render null at the root with createRootErrorUpdate | Log the uncaught error |
These paths are distinct: Suspense represents incomplete asynchronous work, while error boundaries represent captured failures and use class-boundary update behavior.
Sources: packages/react-reconciler/src/ReactFiberThrow.js:93-112, packages/react-reconciler/src/ReactFiberThrow.js:114-118, packages/react-reconciler/src/ReactFiberThrow.js:120-201
How it connects
The work loop is the entry point that receives the thrown value and delegates boundary selection and capture to throwException. This places the behavior inside the render process described in Render Phase and the scheduling decisions described in Work Loop & Scheduling.
Suspense state is later observed by traversal logic such as findFirstSuspended, which identifies Suspense fibers with active suspended state or captured Suspense-list work. The resulting fallback and retry behavior eventually crosses into the commit machinery described in Commit Phase.
The thrown-value path also relies on fiber fields, alternates, flags, lanes, and parent relationships, so its state changes are part of the model explained in Fiber Architecture.
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:3247-3257, packages/react-reconciler/src/ReactFiberSuspenseComponent.js:63-107
Key takeaways
- A thrown object with
thenfollows the Suspense path; other thrown values follow error capture. - Suspense resets incomplete work, marks a boundary, renders a fallback pass, and retries after resolution.
- Error boundaries receive
CaptureUpdateand recover throughgetDerivedStateFromErrororcomponentDidCatch. - Without a handling boundary, the root renders
nulland logs the uncaught error.
Sources: packages/react-reconciler/src/ReactFiberThrow.js:114-118, packages/react-reconciler/src/ReactFiberThrow.js:120-201, packages/react-reconciler/src/ReactFiberThrow.js:93-112