Error Boundaries
An error boundary is a React component that catches JavaScript errors thrown while rendering its child subtree, logs them, and displays a fallback UI instead of letting the whole app crash. Without one, a single uncaught render error unmounts the entire React tree and leaves users staring at a blank screen. Error boundaries let you contain failures to a region of the page, so a broken widget does not take down the navigation, the sidebar, and everything else around it.
Why error boundaries exist
In a component tree, an exception thrown during render normally propagates upward and aborts the commit. Starting with React 16, an unhandled render error unmounts the whole tree on purpose — a half-broken UI is considered worse than no UI, because it can show stale or corrupted data. Error boundaries give you a recovery point: a place to stop the propagation, render something safe, and report the problem.
You typically place boundaries around meaningful sections — a route, a dashboard panel, a comments list — so each region fails independently.
What they catch and what they do not
Error boundaries only catch errors during the render phase, in lifecycle methods, and in constructors of the components below them. They deliberately do not catch several common error sources, because those run outside React’s rendering flow.
| Error source | Caught by an error boundary? | How to handle instead |
|---|---|---|
| Rendering a child component | Yes | Fallback UI |
| A child’s lifecycle / hook body | Yes | Fallback UI |
Event handlers (onClick, etc.) | No | try/catch in the handler |
setTimeout / requestAnimationFrame | No | try/catch in the callback |
Async code (await, .then) | No | try/catch, or .catch() |
| Errors thrown in the boundary itself | No | A boundary above it |
| Server-side rendering | No (catch on the server) | Wrap renderToString etc. |
Warning: The most common surprise is event handlers. A
throwinsideonClickis a normal JavaScript exception that React does not intercept — wrap risky handler code intry/catchand update state to show the error yourself.
Building an error boundary
There is no hook-based error boundary; this is the one place where a class component is still required, because the behavior depends on static getDerivedStateFromError and componentDidCatch, which have no hook equivalents.
import { Component } from "react";
class ErrorBoundary extends Component {
state = { error: null };
// Render phase: derive fallback state from the thrown error.
static getDerivedStateFromError(error) {
return { error };
}
// Commit phase: side effects like logging belong here.
componentDidCatch(error, info) {
console.error("Caught by boundary:", error, info.componentStack);
// sendToErrorService(error, info.componentStack);
}
reset = () => this.setState({ error: null });
render() {
if (this.state.error) {
return (
<div role="alert">
<p>Something went wrong: {this.state.error.message}</p>
<button onClick={this.reset}>Try again</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
getDerivedStateFromError runs during rendering, so it must be pure — return new state, do nothing else. componentDidCatch runs after the error is committed and is the right place for logging or analytics. Wrap any subtree with it:
function App() {
return (
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
);
}
If Dashboard (or anything it renders) throws during render, the user sees the fallback instead of a white screen.
Output:
Caught by boundary: TypeError: Cannot read properties of undefined (reading 'name')
at ProfileCard (Dashboard.jsx:12)
Using react-error-boundary
Writing class components by hand is repetitive, so most teams use the react-error-boundary package, which wraps the class logic in a clean, hook-friendly API with built-in reset support.
npm install react-error-boundary
import { ErrorBoundary } from "react-error-boundary";
function Fallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Failed to load: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={Fallback}
onError={(error, info) => logError(error, info.componentStack)}
>
<Dashboard />
</ErrorBoundary>
);
}
It also exposes useErrorBoundary, which lets you forward errors from outside render — such as an async fetch — into the nearest boundary:
import { useErrorBoundary } from "react-error-boundary";
import { useEffect } from "react";
function Profile({ id }) {
const { showBoundary } = useErrorBoundary();
useEffect(() => {
fetch(`/api/users/${id}`)
.then((res) => res.json())
.catch(showBoundary); // routes the async error to the boundary
}, [id, showBoundary]);
return <p>Loading…</p>;
}
Resetting after an error
A boundary stays in its error state until you reset it. The retry button above clears local state, but if the same bad props produce the same error, you will just fail again. Use resetKeys to automatically recover when a relevant value changes — for example, retrying when the user navigates to a different record.
<ErrorBoundary
FallbackComponent={Fallback}
resetKeys={[userId]} // reset whenever userId changes
onReset={() => queryClient.invalidateQueries(["user", userId])}
>
<Profile id={userId} />
</ErrorBoundary>
Tip: Pair error boundaries with
<Suspense>. Suspense handles the loading state of a subtree while an error boundary handles its failed state — together they cover both async outcomes cleanly.
Best practices
- Place boundaries around independent regions (routes, panels, widgets) rather than wrapping the whole app in one, so failures stay contained.
- Use
componentDidCatch(oronError) to report errors to a logging service — boundaries are your last chance to capture stack and component-stack data. - Remember boundaries do not catch event-handler, async, or SSR errors; handle those with
try/catchand surface them via state. - Prefer
react-error-boundaryover hand-rolled classes for reset keys,useErrorBoundary, and a tested API. - Provide a real recovery path (retry button or
resetKeys) instead of a dead-end message. - Keep fallback UIs lightweight and side-effect free so they cannot throw and trigger a parent boundary.
- In development, expect React’s overlay to also show the error; in production only your fallback appears.