What Are Error Boundaries and How Do They Work?
Advertisement
🧩 Scenario
Architecture Walkthrough
The Problem Error Boundaries Solve
Without error boundaries, a JavaScript error thrown during rendering in any component propagates up the component tree and eventually reaches React, which unmounts the entire React root and shows a blank page. A single malformed API response, a null pointer dereference, or a prop with an unexpected type in one widget can crash the whole application.
Error boundaries are React's answer to graceful degradation. A boundary wrapping a subtree catches any error thrown during rendering within that subtree and renders a fallback instead of propagating the crash upward. The rest of the application, outside the boundary, continues to function normally.
How They Work Internally
When a child throws during render, React walks up the component tree looking for the nearest class component that implements getDerivedStateFromError. It calls this static method with the error, receives the state update, and re-renders the boundary with hasError: true, rendering the fallback instead of the children.
After committing the fallback to the DOM, React calls componentDidCatch(error, info) on the boundary, passing the error and a componentStack string showing the component hierarchy where the error occurred. This is the appropriate place for error reporting.
The Limits of Error Boundaries
Error boundaries have four hard limits. They do not catch errors in event handlers (handlers run in the browser event system, outside React's render cycle; use try/catch in the handler). They do not catch errors in async code (a rejected promise or thrown error inside setTimeout, fetch, or async/await is not a render error; catch it where it occurs). They do not catch SSR errors. And they do not catch errors thrown by the error boundary component itself.
Key Code Explained
// Conceptual model: what an error boundary does
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
// Phase 1 — Render phase: flip state to show fallback
static getDerivedStateFromError(_error: Error) {
return { hasError: true };
}
// Phase 2 — Commit phase: safe to log, call APIs, send to Sentry
componentDidCatch(error: Error, info: React.ErrorInfo) {
Sentry.captureException(error, {
extra: { componentStack: info.componentStack },
});
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
// The isolation strategy: per-section boundaries
function CheckoutPage() {
return (
<main>
<CheckoutHeader />
{/*
One boundary wrapping everything means: OrderSummary throws,
the entire checkout form disappears — the user sees nothing.
*/}
<ErrorBoundary fallback={<p>Checkout unavailable</p>}>
<OrderSummary />
<ShippingForm />
<PaymentForm />
</ErrorBoundary>
{/*
Three separate boundaries mean: OrderSummary throws,
only the summary shows an error. Payment and Shipping still work.
*/}
<ErrorBoundary fallback={<SummaryError />}>
<OrderSummary />
</ErrorBoundary>
<ErrorBoundary fallback={<ShippingError />}>
<ShippingForm />
</ErrorBoundary>
<ErrorBoundary fallback={<PaymentError />}>
<PaymentForm />
</ErrorBoundary>
</main>
);
}
// What error boundaries DO NOT catch — and the correct alternative
// 1. Event handler errors: use try/catch inside the handler
function DeleteButton({ id }: { id: string }) {
const handleClick = async () => {
try {
await deleteItem(id);
} catch (err) {
// Error boundary will NOT catch this — it runs outside the render cycle
// Handle it here instead
showToast('Delete failed. Please try again.');
}
};
return <button onClick={handleClick}>Delete</button>;
}
// 2. Async errors from useEffect: catch inside the async function
function DataLoader({ id }: { id: string }) {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function load() {
try {
const data = await fetchData(id);
setData(data);
} catch (err) {
// Error boundary will NOT catch this — it is async, outside render
setError('Failed to load data');
}
}
load();
}, [id]);
if (error) return <ErrorMessage message={error} />;
// ...
}
// 3. Using react-error-boundary for function component API
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
function RiskyFeature() {
const { showBoundary } = useErrorBoundary();
const handleAction = async () => {
try {
await someAsyncAction();
} catch (err) {
// Programmatically trigger the nearest error boundary
showBoundary(err);
}
};
return <button onClick={handleAction}>Do Risky Thing</button>;
}
The useErrorBoundary() hook from react-error-boundary bridges the gap between async errors and error boundaries. You can catch an async error in a try/catch and then call showBoundary(err) to programmatically trigger the nearest boundary. This gives async errors the same fallback treatment as synchronous render errors.
Tradeoffs
| Error source | Error boundary catches | Correct alternative |
|---|---|---|
| Render error | Yes | Error boundary is correct |
| Event handler error | No | try/catch inside the handler |
| useEffect async error | No | try/catch inside async function |
| setTimeout / promise | No | try/catch or .catch() |
| SSR error | No | Server-side error handling |
What Interviewers Actually Check
- Whether you can explain what problem error boundaries solve
- Whether you know
getDerivedStateFromErrorvscomponentDidCatchand when each runs - Whether you can list what they do not catch with the correct alternative
- Whether you know the isolation strategy (per-section vs whole-app)
- Whether you know
react-error-boundaryas the standard library
Follow-Up Questions
- How does React 18's
startTransitioninteract with error boundaries — do errors thrown in transitions still surface to the nearest boundary? - How would you build a global error reporting system that captures all boundary errors and sends them to Sentry with user context?
- What is the difference between an error boundary and a
window.onerrorevent listener in terms of what they catch? - How do you test that an error boundary renders its fallback correctly in a Jest + React Testing Library test?
- How would you implement a boundary that shows different fallback UI based on the error type (network error vs parse error vs auth error)?
Common Candidate Mistakes
- Saying error boundaries catch all JavaScript errors in a React app — they only catch synchronous render and lifecycle errors
- Not knowing that event handler errors must be caught with try/catch inside the handler
- Thinking a single app-level boundary is a complete error strategy — one boundary means any error shows an app-level fallback, not a local one
- Confusing the two lifecycle methods: candidates often say
componentDidCatchis for updating state, but that role belongs togetDerivedStateFromError - Not knowing
react-error-boundaryas a practical alternative to writing class components from scratch
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain what an error boundary is and what crash scenario it prevents?
- Can you describe
getDerivedStateFromError(render phase, returns state update) vscomponentDidCatch(commit phase, for logging)? - Can you list four sources of errors that error boundaries do not catch, with the correct alternative for each?
- Can you explain why per-section isolation is better than a single app-level boundary?
- Can you mention
react-error-boundaryand what it adds over a manual class boundary?
Summary
An error boundary is a class component that prevents render errors in its subtree from crashing the entire React application. Without boundaries, any unhandled render error propagates up and causes React to unmount the whole tree, showing a blank page. With boundaries wrapping independent sections, only the failing section shows a fallback while the rest of the UI remains functional.
Two lifecycle methods implement the behavior. getDerivedStateFromError runs during the render phase when a child throws, receives the error, and returns a state update that flips the boundary to show its fallback on the same render. componentDidCatch runs after commit, receives the error and component stack, and is the correct place for side effects like logging to Sentry or a custom error service.
Error boundaries have hard limits: they only catch synchronous render and lifecycle errors. Event handler errors must be caught with try/catch inside the handler. Async errors from useEffect, setTimeout, or fetch must be caught where they occur and either handled locally or surfaced to a boundary programmatically via react-error-boundary's showBoundary. SSR errors are outside their scope entirely. A complete production error strategy combines boundaries (render isolation), try/catch (async handlers), Sentry (logging), and retry logic (recovery).
Why do error boundaries not catch errors in event handlers?
Error boundaries catch errors that occur during rendering. Event handlers run outside the React render cycle (they are called by the browser event system). React never executes event handler code during a render pass, so there is nothing for the boundary to intercept. Use try/catch inside the handler instead.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement