React Error Boundaries: A Practical Guide
How to build resilient React apps with Error Boundaries: what they catch, what they miss, and how to design fallback UI that actually helps users.
What you'll learn
- ✓What Error Boundaries catch
- ✓Why they are still class components
- ✓How to compose them with Suspense
- ✓Designing useful fallbacks
- ✓Logging and reset patterns
Prerequisites
- •Comfortable with React components
An uncaught error during rendering will, by default, unmount your entire React tree. The user sees a blank page, which is rarely what you want. Error Boundaries are React’s mechanism to catch those errors, isolate the broken subtree, and render a fallback instead. They are simple to write, easy to forget, and worth setting up early.
What and why
An Error Boundary is a component that implements getDerivedStateFromError or componentDidCatch. When a descendant throws during render, in a lifecycle method, or in a constructor, the nearest boundary catches it and renders its fallback. The rest of the app keeps running.
What boundaries do not catch: event handlers, asynchronous code (like setTimeout), server-side rendering errors, and errors thrown in the boundary itself. For event handlers and async code, you handle errors with try/catch like normal JavaScript.
Mental model
A boundary is a try/catch for the rendering phase. When a child throws, React walks up the tree until it finds a boundary, then renders that boundary’s fallback in place of the broken subtree.
<ErrorBoundary fallback={<Oops />}>
|
v
<Dashboard>
|
v
<Chart> <-- throws during render
|
v
React unwinds
|
v
ErrorBoundary catches -> renders <Oops />
(rest of app outside boundary is untouched) The granularity matters. A single boundary at the root will replace the whole app on any error. Multiple smaller boundaries let you isolate the damage to a widget.
Hands-on example
A minimal boundary still has to be a class component because React has not shipped a hook equivalent for componentDidCatch.
import { Component, ReactNode } from 'react';
type Props = { fallback: ReactNode; onError?: (e: Error) => void; children: ReactNode };
type State = { hasError: boolean };
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error, info: { componentStack: string }) {
this.props.onError?.(error);
console.error('Caught:', error, info.componentStack);
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
Usage with a meaningful fallback and a reset:
function App() {
const [key, setKey] = useState(0);
return (
<ErrorBoundary
key={key}
fallback={
<div>
<p>Something went wrong loading the chart.</p>
<button onClick={() => setKey(k => k + 1)}>Try again</button>
</div>
}
onError={(e) => logToService(e)}
>
<RevenueChart />
</ErrorBoundary>
);
}
Changing the key forces React to unmount and remount the boundary, which resets its internal hasError flag and gives the subtree a fresh start.
Composing with Suspense
Suspense handles “not ready yet”; Error Boundaries handle “broken.” Pair them so each loading region also has a recovery path.
<ErrorBoundary fallback={<ListError />}>
<Suspense fallback={<ListSkeleton />}>
<OrderList />
</Suspense>
</ErrorBoundary>
If the fetch fails, the error boundary shows <ListError />. If it is still loading, Suspense shows the skeleton. The two responsibilities stay separate.
For a smoother developer experience, libraries like react-error-boundary provide a hook-based API and reset helpers.
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => refetch()}
resetKeys={[queryId]}
>
<Widget />
</ErrorBoundary>
Common pitfalls
- Wrapping the whole app in one boundary. A single error nukes everything. Use multiple boundaries at meaningful UI seams: routes, panels, widgets.
- Catching errors in event handlers with a boundary. Boundaries do not catch those; use
try/catchinside the handler. - Forgetting to log. A boundary that silently swallows errors makes production debugging painful. Always pipe
componentDidCatchto your monitoring service. - Showing a generic “something went wrong” with no recovery action. Give users a retry button or a path back to a known good state.
Best practices
- Put boundaries at route level by default, then add more at risky widgets (charts, embeds, third-party content).
- Log the error and the component stack to your monitoring service. The component stack is more useful than the JavaScript stack for finding the offending component.
- Use
resetKeysorkeyto allow recovery without a full page reload. - For server components in frameworks like Next.js, use the framework-provided
error.tsxconvention, which maps to the same idea.
FAQ
Do hooks support error boundaries? No native hook exists. You either use a class component or pull in react-error-boundary.
Do boundaries work in SSR? They catch errors during hydration on the client. Server-side render errors are handled differently, typically by the framework’s error page.
Should I rethrow in componentDidCatch? No. Throwing inside a boundary bubbles to the next boundary up and can confuse the error reporting flow.
Can a boundary catch async errors from fetch? Only if the async result causes a render-time throw (for example, when using Suspense and use()). For raw fetch().then() errors, handle them in the callback.
Related articles
- React React Error Boundaries: Catching Render Errors
Build a class-based React error boundary, understand getDerivedStateFromError and componentDidCatch, design fallback UI, and learn why async errors need different handling.
- React React Context vs Redux: When to Use Which
A practical comparison of React Context and Redux: rendering model, performance, devtools, and concrete heuristics for picking the right tool.
- React React Forms: Controlled vs Uncontrolled Inputs Explained
Understand the difference between controlled and uncontrolled inputs in React, when to use each, and how to combine them with refs and form libraries.
- React The React key Prop: A Deep Dive Into Identity and Reconciliation
Understand what the React key prop actually does, how it drives reconciliation, and why a bad key choice causes mysterious state and animation bugs.