What are error boundaries in React?
The short answer
An error boundary is a React component that catches JavaScript errors anywhere in its child component tree and renders a fallback UI instead of crashing the entire application. Think of it as a try/catch block, but for your component tree.
Without error boundaries, a single runtime error in one tiny component can unmount your whole app and leave users staring at a blank white screen. Error boundaries let you contain the blast radius.
Why they exist
Before React 16, there was no good way to handle render-time errors. If a component threw during rendering, React would leave behind a corrupted UI that could behave unpredictably. The React team decided it was better to show nothing than to show a broken UI — so unhandled errors started unmounting the entire tree.
Error boundaries give you a way to say: "If something goes wrong inside this section, show a fallback instead of killing the whole page."
How to build one
An error boundary is a class component (there's no hook equivalent — more on that later) that implements one or both of these lifecycle methods:
static getDerivedStateFromError(error)— runs during rendering. Use it to flip a flag in state so the next render shows your fallback.componentDidCatch(error, errorInfo)— runs after the error is committed. Use it for side effects like sending the error to your logging service.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Caught by boundary:', error);
console.error(
'Component stack:',
errorInfo.componentStack
);
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<h2>Something went wrong.</h2>
)
);
}
return this.props.children;
}
}Then wrap any part of your tree:
function App() {
return (
<div>
<Header />
<ErrorBoundary
fallback={<p>This section failed to load.</p>}
>
<Dashboard />
</ErrorBoundary>
<Footer />
</div>
);
}If Dashboard or anything inside it throws during rendering, the boundary catches it and shows the fallback. Header and Footer keep working. The damage is contained.
What they catch — and what they don't
This is the part that trips people up. Error boundaries catch errors during:
- Rendering (the return statement of your component)
- Lifecycle methods (
componentDidMount,componentDidUpdate, etc.) - Constructors of child components
They do not catch:
- Event handlers — a click handler that throws won't trigger the boundary. These don't happen during rendering, so you need a regular
try/catch. - Async code — errors inside
setTimeout,fetch().then(...), orasync/awaitaren't caught. - Server-side rendering
- Errors in the boundary itself — a boundary can't catch its own errors, only its children's.
function Button() {
const handleClick = () => {
try {
riskyOperation();
} catch (error) {
// Handle it yourself — error boundaries can't help here
}
};
return <button onClick={handleClick}>Click me</button>;
}The mental model is simple: if the error happens during React's render cycle, the boundary catches it. If it happens outside of rendering (user interactions, network calls, timers), it doesn't.
Where to place them
You can be as broad or as granular as you want:
- App level — wrap the entire app to prevent the dreaded white screen. This is the bare minimum every app should have.
- Route level — wrap each page so a crash on the settings page doesn't break the dashboard.
- Feature level — wrap individual widgets like charts, comment sections, or third-party embeds that you don't fully trust.
Most teams use a combination. A top-level boundary as a safety net, with more specific boundaries around features that are more likely to fail.
Letting users recover
A nice pattern is giving users a "Try again" button. If the error was transient (maybe a data fetch returned something unexpected on one render), resetting the boundary's state gives the children another chance:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return (
<div>
<p>Something went wrong.</p>
<button
onClick={() =>
this.setState({ hasError: false })
}
>
Try again
</button>
</div>
);
}
return this.props.children;
}
}Clicking "Try again" flips hasError back to false, React re-renders the children, and if the underlying issue is gone, the app recovers without a full page reload.
Why it has to be a class component
There's no hook for getDerivedStateFromError or componentDidCatch. The React team hasn't added one yet. This means error boundaries are one of the last remaining reasons to write a class component in a modern React codebase.
In practice, most teams either write one error boundary class and reuse it everywhere, or use the react-error-boundary library which wraps this pattern with extra features like automatic reset on navigation and a useErrorBoundary hook for event handler errors.
Why interviewers ask this
Error boundaries test whether you understand React's error handling model, the difference between render-time and runtime errors, and how to build resilient UIs. It also touches on why class components still exist, which shows you understand React's evolution rather than just its current API.