Concept
The beginner framing: <Suspense> lets you show a loading fallback while a part of your UI isn't ready yet, its data hasn't arrived, or its code hasn't finished downloading, and swaps in the real content once it is.
The precise mental model: a component signals "I'm not ready" by throwing a promise during render, instead of returning JSX. This is a deliberate, sanctioned use of throw as a coordination signal, not an error, the nearest ancestor <Suspense> catches that thrown promise (much like an error boundary catches a thrown Error, see Error Boundaries) and commits its fallback in the meantime. Once the promise resolves, React re-attempts rendering that component from scratch, and this time, since the data is ready, it returns real content instead of throwing.
<Suspense fallback={<Spinner />}><ProfileDetails /></Suspense>
ProfileDetails calls a special resource-reading function to get its data. This is the very first render attempt.
React.lazy: the original, well-established Suspense use case
const SettingsPanel = React.lazy(() => import("./SettingsPanel"));
function App() {
return (
<Suspense fallback={<Spinner />}>
<SettingsPanel />
</Suspense>
);
}React.lazy wraps a dynamic import() so that the component's code is only downloaded when it's first rendered, while that download is in flight, React.lazy throws the import's promise, and <Suspense> shows the fallback exactly like it would for a data-fetching component. This is code-splitting and Suspense working through the identical throw/catch mechanism, just suspending on a code download instead of a data fetch.
use(): the officially-supported way to read a promise during render
As of React 19, the use() hook is the sanctioned way to read a promise (or a context) directly during render, const data = use(promise);, and it uses the same underlying throw/catch mechanism internally: if the promise is still pending, use() throws it for the nearest Suspense boundary to catch, exactly like the hand-rolled pattern below.
Streaming SSR: the same mechanism, on the server
<Layout><Header /><Suspense fallback={<Skeleton />}><SlowWidget /> {/* data not ready on the server yet */}</Suspense></Layout>
The server doesn't wait for SlowWidget's data before sending anything, it streams the shell HTML plus the fallback's HTML right away, so the browser can paint immediately.
Server-side, Suspense lets the server send the parts of the page that are ready immediately, without waiting for the slowest piece, the fallback's HTML is streamed first, and a follow-up chunk with the real content (plus a tiny inline script that swaps it into place) arrives over the same connection once the data resolves.
Try It
Predict what happens before checking the solution.
<Suspense fallback={<Spinner />}>
<UserProfile /> {/* suspends for 200ms */}
<UserPosts /> {/* suspends for 2000ms */}
</Suspense>Does the fallback disappear after 200ms, once UserProfile is ready, or does it wait longer?
Solution
It waits the full 2000ms. A single <Suspense> boundary shows its fallback until every suspending descendant inside it is ready, not just the first one. UserProfile resolving at 200ms doesn't end the fallback while UserPosts (inside the same boundary) is still pending. To let UserProfile appear as soon as it's ready, independent of UserPosts, they'd need separate Suspense boundaries, one wrapping each.