Suspense — throw a promise, catch a fallback

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ useEffect syncs with the world (imperative: "mount → fetch → setState"). frontend/react: effects & lists shows that manual dance; Suspense waits for data declaratively — you say "render this, or that fallback if it isn't ready" and React handles the rest.

the throw-promise mechanism

A component throws a promise during render when the data it needs isn't ready. React catches the thrown promise, walks UP the tree to the nearest <Suspense>, and renders its fallback instead. When the promise resolves, React re-renders the suspended subtree — now the data is there, so read() returns it and the real UI paints. No isLoading flags, no useEffect+setState, no race conditions: the loading state is a property of the tree, not a field you manage.

APIwhat it suspends onresolves when
React.lazy(() => import()) a component module the dynamic import() promise settles
use(promise)  (React 19) a Promise (or Context) the awaited promise resolves
resource.read()  (this demo) any cached promise the wrapper flips status to 'success'
<Suspense fallback> — (the catcher, not the thrower) the nearest child stops suspending

1 · the resource cache you write (edit me)

2 · Babel compiles JSX → element tree

<Suspense fallback={...}> becomes React.createElement(Suspense, {fallback: ...}). The thrown promise is invisible at the JSX level — it lives inside resource.read(), which Babel leaves untouched as a plain function call.

// (hit "compile & render" to see Babel's output)

3 · live React (the throw-promise round-trip, proven)

On mount, DataViewer calls resource.read() — the promise is pending, so it throws. Suspense catches it and renders the Loading… fallback. ~600ms later the promise resolves, React retries, and Loaded: Hello Suspense! paints. Click Refetch to swap in a fresh resource and watch the round-trip again. The gold-check automates all of this.

state: · gold: fallback → "Hello Suspense!" → Refetch → "Reload #2 OK"

intent → Suspense pattern

intentpatternwhy
lazy-load a component const C = lazy(() => import('./C'))
<Suspense fallback={...}><C/></Suspense>
suspends until the chunk downloads; no waterfall on route change
read a promise during render const data = use(promise) React 19 native; throws the promise, Suspense catches — no manual cache needed if the promise is cached upstream
avoid refetch on every render cache the promise (useState(() => createResource(fetch()))) Suspense re-renders the subtree; an uncached promise re-suspends forever
isolate loading regions nest <Suspense> the nearest ancestor's fallback wins; outer content stays interactive while a child loads
handle fetch errors wrap <ErrorBoundary> around <Suspense> a thrown error from a suspended component bypasses Suspense and bubbles to the boundary
keep showing old data while refetching useTransition + <Suspense> the transition holds the old UI until the new resource resolves; fallback only on first load