Testing Error Boundaries and Suspense
Learn to test Suspense and Error Boundaries by validating committed fallback, error, and recovery phases rather than implementation details, even when rendering is time-based and concurrent.
As React applications scale, rendering is no longer a single uninterrupted process. Components may suspend while waiting for data. Nested regions may stream in progressively. Errors may be thrown during rendering and caught by boundaries designed to isolate failure. React 19 formalizes this model: rendering is scheduled, interruptible, and coordinated across boundaries.
In small demos, loading and error states are often tested with simplistic assumptions: wait 500ms, expect a spinner; mock an error, expect a message. But production Suspense flows are more complex. Fallbacks appear and disappear. Errors may surface in nested regions while the outer shell remains stable. Retry flows must reset boundaries without tearing down unrelated UI. Time-based behavior, such as a delayed fallback to avoid flicker, adds even more nuance.
The actual problem we must solve is this:
How do we test Suspense and Error Boundaries in a way that validates the user-visible contract without tying tests to timing hacks or internal mechanics?
Suspense and Error Boundaries define rendering seams. A Suspense boundary commits a fallback when a child suspends. An Error Boundary commits an alternative UI when a descendant throws. Both introduce explicit phases into rendering. Our tests must assert against those committed phases:
What renders before suspension?
When does fallback appear?
What persists during failure?
What resets on retry?
What changes when time thresholds are involved?
Visualize a component tree with two seams: a Suspense boundary around a data panel and an Error Boundary wrapping that panel. When data loading begins, the Suspense fallback replaces only the panel, not the entire page. If the panel throws an error, the Error Boundary replaces that region with an error UI. When the user presses Retry, the boundary resets, and the Suspense cycle may repeat.
Rendering proceeds in visible phases: Stable shell → Fallback → Content or Error → Reset → Retry.
Testing Suspense and Error Boundaries by What React Commits
Suspense is not a data-fetching API; it’s a rendering coordination primitive. When a child throws a promise during render, React pauses that subtree and renders the nearest Suspense fallback instead. Once the promise resolves, React retries the suspended tree and commits its real content.
An Error Boundary works similarly, but instead of catching promises, it catches thrown errors during render, lifecycle methods, or effects. When a descendant throws an error, the boundary commits an error UI instead of crashing the entire tree. A reset action allows React to attempt rendering the subtree again.
Testing Suspense requires reasoning about commit phases:
Initial commit before suspension
Fallback commit during suspension
Final content commit after ...