Why Error Boundaries Exist
Before error boundaries landed in React 16, a single uncaught render error would unmount the entire React tree and leave users staring at a blank screen. There was no way to isolate the failure — one bad component took down everything.
Error boundaries are React's answer to fault isolation. They act as circuit breakers: catch a render error in a subtree, display a fallback UI for that section, and let everything outside the boundary keep running normally.
<App>
<Header /> {/* unaffected — still renders */}
<ErrorBoundary fallback={<p>Widget failed.</p>}>
<ErrorWidget /> {/* throws → fallback shown for this section only */}
</ErrorBoundary>
<Footer /> {/* unaffected — still renders */}
</App>
This is why interviewers ask about them: they touch React internals (lifecycle phases, class components, concurrent mode), real-world fault tolerance, and external service integration (Sentry).
The Two Lifecycle Methods
An error boundary is a class component that implements one or both of two lifecycle methods:
getDerivedStateFromError
Called during the render phase when a descendant throws. Receives the error and must return a state update — typically a flag that switches the component to render its fallback.
static getDerivedStateFromError(error) {
return { hasError: true, error }; // pure — no side effects
}
It runs during rendering so it must be pure — no console.log, no fetch calls.
componentDidCatch
Called during the commit phase, after the fallback has been painted. Receives both the error and an info object with componentStack — the React component call stack. This is the correct place for side effects like error logging.
componentDidCatch(error, info) {
logErrorToSentry(error, {
extra: { componentStack: info.componentStack },
});
}
The two methods are complementary: getDerivedStateFromError decides what to show; componentDidCatch handles the side effects. Use both.
Why Class Components Are Required
Error boundaries require these two lifecycle methods, which have no hook equivalents in React's public API. As of React 18 you still need a class component somewhere in the chain. The React team has acknowledged the gap but has not shipped a hook-based alternative.
In practice, the react-error-boundary library wraps a class boundary behind a clean functional API — this is the recommended approach for modern codebases that want to avoid writing class components directly.
What Error Boundaries Do NOT Catch
This is a favourite interview question. Boundaries catch errors during rendering, constructors, and lifecycle methods in the subtree below them. They do not catch:
- Event handlers —
onClick,onChange, etc. run outside React's render cycle. Use try/catch inside them. - Async code — errors in
setTimeout, resolved/rejected Promises,async/awaitare not caught because they happen outside React's call stack. - Server-side rendering — boundaries work client-side only.
- Errors in the boundary itself — a boundary cannot catch its own render errors; those propagate to the next ancestor boundary.
function BadButton() {
const handleClick = () => {
try {
riskyOperation(); // boundary WON'T catch this — use try/catch
} catch (err) {
reportError(err);
}
};
return <button onClick={handleClick}>Go</button>;
}
Boundary Granularity
Where you place boundaries determines how much of the UI survives a failure. A good strategy uses three levels:
- Root level — a catch-all backstop. If everything else fails, prevent a blank screen.
- Route/page level — a boundary per route. A broken settings page doesn't kill the dashboard.
- Widget/feature level — around autonomous sections (recommendation panels, ad slots, live feeds) that can fail without affecting main content.
The rule: place a boundary wherever you can write a meaningful, scoped fallback message. The finer the boundary, the better the user experience.
Fallback UI Design
A good fallback tells users what happened, what they can do next, and ideally provides a recovery action.
function FallbackUI({ error, resetErrorBoundary }) {
return (
<div role="alert" className="error-panel">
<h2>This section couldn't load.</h2>
<p>Try refreshing or come back later.</p>
<button onClick={resetErrorBoundary}>Try again</button>
{/* Never expose error.message in production */}
</div>
);
}
Keep fallback UI dead simple — never fetch data or render lazily inside it. If the fallback itself throws, the error propagates to the next ancestor boundary. Plain HTML is safest.
Error Recovery
Recovery works by resetting the boundary's error state so it re-renders its children. The key subtlety: if nothing changed since the crash, the component will throw again immediately. Pair the reset with a data refetch or a key change to give the retry a real chance to succeed.
The react-error-boundary Library
react-error-boundary is the community standard. It provides:
<ErrorBoundary>withFallbackComponent,fallback,onError,onReset, andresetKeyspropsFallbackComponentreceives{ error, resetErrorBoundary }— the fallback can trigger recovery without prop drillingresetKeys— an array of values; when any changes, the boundary auto-resets (data-driven recovery)useErrorBoundary()— lets functional components escalate async errors into the nearest boundary
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary
FallbackComponent={({ error, resetErrorBoundary }) => (
<div role="alert">
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
resetKeys={[userId]} // auto-reset when user switches
onError={(err, info) => Sentry.captureException(err, { extra: info })}
>
<UserProfile userId={userId} />
</ErrorBoundary>
useErrorBoundary for Async Errors
showBoundary(error) from useErrorBoundary escalates errors that happen outside the render cycle — the one category boundaries miss natively:
function UserList() {
const { showBoundary } = useErrorBoundary();
useEffect(() => {
fetchUsers()
.then(setUsers)
.catch(err => showBoundary(err)); // route async error into boundary system
}, []);
// ...
}
Error Boundaries + Suspense
<Suspense> handles loading states (a component suspends by throwing a Promise). Error boundaries handle error states (a component throws an Error). The two compose naturally — wrap Suspense inside an ErrorBoundary to cover both failure modes for the same subtree:
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Spinner />}>
{/* Suspends while loading → shows Spinner */}
{/* If it throws an Error → ErrorBoundary catches it */}
<LazyComponent />
</Suspense>
</ErrorBoundary>
Every <Suspense> boundary should have an <ErrorBoundary> wrapper — Suspense only handles the pending state.
Development vs Production Behaviour
In development, React re-throws errors after calling the boundary so the browser DevTools overlay and console both show the full stack trace. You will see the error in the console even when a boundary catches it — this is intentional, not a bug.
In production, the error is caught silently. Users see only the fallback; developers see nothing unless logging is configured. This is why componentDidCatch / onError is essential — it is your only visibility into boundary-caught errors in production.
Testing Error Boundaries
Render a component that intentionally throws and assert the fallback appears. Suppress console.error to keep test output clean, but always restore the spy.
function Bomb() { throw new Error('test error'); }
test('renders fallback when child throws', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(
<ErrorBoundary fallback={<p>Boundary caught it</p>}>
<Bomb />
</ErrorBoundary>
);
expect(screen.getByText('Boundary caught it')).toBeInTheDocument();
spy.mockRestore();
});
Key Takeaways
- Error boundaries are class components that implement
getDerivedStateFromError(render-phase, pure, returns state update) and/orcomponentDidCatch(commit-phase, safe for side effects like Sentry logging). - They catch errors during rendering, constructors, and lifecycle methods — not event handlers, async code, SSR, or their own errors.
- Place boundaries at three levels: root (last resort), route (page isolation), widget (feature isolation).
- Fallback UI should be simple, actionable, and never expose raw error messages in production.
- Use
react-error-boundaryin production — it eliminates the need to write class components and handles reset logic correctly. resetKeysenables data-driven recovery — the boundary auto-resets when a resource ID changes.useErrorBoundary+showBoundarybridges async errors into the boundary system.- Wrap every
<Suspense>in an<ErrorBoundary>— they handle different failure modes and compose naturally. - In development, the DevTools overlay appears even when a boundary catches the error — this is expected; test production behavior with a production build.