Loading…
Loading…
Combine React Suspense with Error Boundaries to handle loading states and errors declaratively, replacing manual isLoading and isError patterns.
Tags
This is part of the AI Automation Engineer Roadmap series.
Wrap data-fetching components with Suspense for loading states and ErrorBoundary for error states to build resilient UIs declaratively.
Your components are cluttered with if (isLoading) return <Spinner /> and if (error) return <ErrorMessage /> checks. Every data-fetching component repeats this pattern, and nested loading states create waterfall renders. The imperative approach makes it hard to compose loading and error UI at the layout level.
Suspense and Error Boundaries matter because they let you move loading and error behavior out of leaf components and up into the UI structure itself. That gives you better control over:
That is a meaningful architectural improvement, not just a cleaner syntax.
It helps to separate the roles clearly:
Suspense handles waitingErrorBoundary handles failureSuspense does not catch errors. Error Boundaries do not handle loading. They solve adjacent problems and work best when combined intentionally.
The most practical composition is an Error Boundary wrapped around a Suspense boundary for each independently important section.
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-4">
<ErrorBoundary fallback={<ErrorCard message="Failed to load stats" />}>
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback={<ErrorCard message="Failed to load activity" />}>
<Suspense fallback={<ActivitySkeleton />}>
<ActivityFeed />
</Suspense>
</ErrorBoundary>
</div>
);
}Each section loads independently. If StatsPanel fails, ActivityFeed still renders normally.
// StatsPanel.tsx - no loading/error checks needed
async function StatsPanel() {
const stats = await fetchDashboardStats(); // throws on error
return (
<div>
<h3>Total Users: {stats.userCount}</h3>
<h3>Revenue: ${stats.revenue}</h3>
</div>
);
}The important shift is that StatsPanel focuses on rendering successful data. The loading and failure states are handled by the surrounding boundaries.
import { ErrorBoundary } from "react-error-boundary";
function ErrorCard({ error, resetErrorBoundary }) {
return (
<div className="rounded-lg border-red-500 p-4">
<p>Something went wrong: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Usage with retry capability
<ErrorBoundary FallbackComponent={ErrorCard}>
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
</ErrorBoundary>Retry support matters more than teams usually think. If the fallback is just a dead-end error box, the UI feels brittle. A reset path makes boundary-based error handling much more usable in real products.
Suspense is especially useful when:
It is less useful when the whole page depends on one tiny fetch and extra granularity adds complexity without improving UX.
One of the biggest practical wins is granular composition.
You can use:
The goal is not to wrap everything blindly. The goal is to match the boundary to the user experience you want when a section is loading or broken.
Suspense is a rendering primitive, not a complete data strategy. You still need to think about caching, revalidation, retries, and where data should live.
This often recreates the same bad UX as a full-page spinner. If every section waits on one fallback, you lose the benefit of progressive rendering.
Error boundaries are much stronger when the fallback can reset, retry, or route the user somewhere useful.
If you adopt Suspense but keep isLoading and isError branches in every component, you usually end up with more complexity instead of less.
If you are introducing these patterns in a real app, the clean starting point is:
Suspense catches the promise thrown by data-fetching libraries (React Query, Next.js Server Components, or use()) and shows the fallback until it resolves. Error Boundaries catch rendering errors from child components via the componentDidCatch lifecycle. By composing them declaratively in the component tree, you control loading and error granularity at the layout level rather than inside each component. Nesting boundaries lets independent sections load in parallel instead of creating request waterfalls.
Suspense and Error Boundaries are best thought of as layout-level resilience tools. Use Suspense to decide what can wait, use Error Boundaries to decide what can fail safely, and compose both around the user experience you actually want.
Collaboration
I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI: let's bring your vision to life.
Five essential built-in generic utility types in TypeScript that will save you hundreds of lines of code.
Generate dynamic Open Graph images in Next.js using the ImageResponse API with custom fonts, gradients, and data-driven content for social sharing.
Create reusable GitHub Actions workflows with inputs, secrets, and outputs to eliminate YAML duplication across repositories and teams efficiently.