React Suspense and Error Boundaries: A Quick Guide
Combine React Suspense with Error Boundaries to handle loading states and errors declaratively, replacing manual isLoading and isError patterns.
Tags
React Suspense and Error Boundaries: A Quick Guide
This is part of the AI Automation Engineer Roadmap series.
TL;DR
Wrap data-fetching components with Suspense for loading states and ErrorBoundary for error states to build resilient UIs declaratively.
Why This Matters
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:
- ›what blocks rendering
- ›what can load independently
- ›which parts of a page fail gracefully
- ›how retry and fallback UI are presented
That is a meaningful architectural improvement, not just a cleaner syntax.
What Each One Actually Does
It helps to separate the roles clearly:
- ›
Suspensehandles waiting - ›
ErrorBoundaryhandles failure
Suspense does not catch errors. Error Boundaries do not handle loading. They solve adjacent problems and work best when combined intentionally.
A Good Default Composition
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.
The Data-Fetching Component
// 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.
Reusable Error Boundary with Reset
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.
Where Suspense Helps Most
Suspense is especially useful when:
- ›sections of a page can load in parallel
- ›a skeleton is better than blocking the whole page
- ›you are using Server Components or lazy loading
- ›the layout should stay stable while data streams in
It is less useful when the whole page depends on one tiny fetch and extra granularity adds complexity without improving UX.
Nesting Boundaries Intentionally
One of the biggest practical wins is granular composition.
You can use:
- ›one top-level boundary for a whole route
- ›nested boundaries for expensive dashboard sections
- ›tiny boundaries around risky widgets
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.
Common Mistakes
Treating Suspense as a Universal Data-Fetching Abstraction
Suspense is a rendering primitive, not a complete data strategy. You still need to think about caching, revalidation, retries, and where data should live.
Using One Giant Boundary for the Entire Page
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.
Forgetting Recovery Paths
Error boundaries are much stronger when the fallback can reset, retry, or route the user somewhere useful.
Mixing Imperative Loading Checks Everywhere Anyway
If you adopt Suspense but keep isLoading and isError branches in every component, you usually end up with more complexity instead of less.
Production Recommendations
If you are introducing these patterns in a real app, the clean starting point is:
- ›add one Suspense boundary per meaningful page section
- ›pair each with an Error Boundary when failure should be isolated
- ›use skeletons instead of generic spinners where possible
- ›provide retry behavior in fallbacks
- ›avoid over-fragmenting the tree until the UX clearly benefits
Why This Works
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.
Final Takeaway
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
Need help with a project?
Let's Build It
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.
Related Articles
TypeScript Utility Types You Should Know
Five essential built-in generic utility types in TypeScript that will save you hundreds of lines of code.
Generate Dynamic OG Images in Next.js
Generate dynamic Open Graph images in Next.js using the ImageResponse API with custom fonts, gradients, and data-driven content for social sharing.
GitHub Actions Reusable Workflows: Stop Repeating Yourself
Create reusable GitHub Actions workflows with inputs, secrets, and outputs to eliminate YAML duplication across repositories and teams efficiently.