Suspense & Error Boundaries
Declarative loading. Boundary placement strategy.
<Suspense>used to be exotic. In React 19, it's the standard way to handle anything that takes time: data fetches, lazy-loaded components, images, async values. You wrap a region of your UI in Suspense, give it a fallback, and React shows the fallback while anything inside is still loading.
The basic shape
import { Suspense } from "react";
<Suspense fallback={<Spinner />}>
<UserProfile userId={id} />
</Suspense>While UserProfile (or anything it renders) is suspending, React shows <Spinner />. When everything inside finishes, React swaps it out for the real content. No isLoading state in your component. The component just describes what to render once data exists.
Where to place boundaries
The biggest skill with Suspense is choosing where to put the boundaries. Too few and a single slow fetch hides your whole page. Too many and the UI flickers with spinners everywhere.
Useful guidelines:
- One boundary at the route level for the main content.
- Inner boundaries around independent sections that can load in parallel (sidebar, recommendations, comments).
- Avoid micro-boundaries around every little widget. The flicker tax is real.
- Keep the structure of fallback content similar to the real content (use skeletons) to prevent layout shift.
<Layout>
<Header />
<Suspense fallback={<MainSkeleton />}>
<Main />
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</Suspense>
<Footer />
</Layout>The outer boundary covers the main content. The inner boundary lets comments load independently. The footer renders immediately.
Suspense handles loading. ErrorBoundary handles errors.
Suspense only knows about pending. If your fetch fails, the promise rejects, and you need an ErrorBoundaryto catch it. React still doesn't ship one. The community standard is react-error-boundary:
import { ErrorBoundary } from "react-error-boundary";
<ErrorBoundary fallback={<p>Couldn't load comments</p>}>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</ErrorBoundary>Wrap a region with both: ErrorBoundary on the outside, Suspenseon the inside. If data fails, you see the error fallback. If it's still loading, you see the suspense fallback. Predictable, composable, no isLoading and isError booleans cluttering your components.
Document metadata hoists itself
React 19 added something quietly useful: you can put <title>, <meta>, and <link> tags inside any component, and React hoists them to <head> automatically.
function BlogPost({ post }) {
return (
<article>
<title>{post.title} – My Blog</title>
<meta name="description" content={post.excerpt} />
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}That <title> ends up in the document head. No Helmet, no Next-only API, just JSX. Multiple components writing to the same tag use last-write-wins.
Asset preloading
React 19 also exposed a small set of preloading APIs from react-dom:
import { preload, preconnect, prefetchDNS } from "react-dom";
function App() {
preload("/fonts/inter.woff2", { as: "font", crossOrigin: "anonymous" });
preconnect("https://cdn.example.com");
prefetchDNS("https://api.example.com");
// ...
}These add the equivalent <link rel="preload"> tags to <head>. The browser starts fetching the asset in parallel with React rendering. Critical for fonts, hero images, and CDN connections.
Try it: nested suspense with a fake async tree
Two cards. Each loads independently. The outer suspense holds the page until both have started rendering. Watch them stream in.
Quiz
What does <Suspense fallback={x}> show its fallback for?
Recap
<Suspense fallback>shows a placeholder while descendants suspend. Use for loading states, lazy components, async data.- Choose boundary placement carefully. One per logical region, not one per widget.
- Suspense handles pending.
ErrorBoundaryhandles errors. Wrap with both for resilience. - Place
<title>,<meta>,<link>inline; React 19 hoists them to<head>. - Use
preload,preconnect, andprefetchDNSfromreact-domfor critical assets.