Error Boundaries
What Are Error Boundaries?
Without error handling, a JavaScript error thrown inside a component crashes the entire React tree — the user sees a blank page. Error Boundaries are components that catch errors anywhere in their child component tree and display a fallback UI instead.
Think of them as a try/catch for your component tree.
Without Error Boundary:
App → Header → Main → BrokenWidget
↓ throws
BLANK PAGE ❌
With Error Boundary:
App → Header → Main → ErrorBoundary → BrokenWidget
↓ catches error
"Something went wrong" ✅ (rest of app works)
Creating an Error Boundary
Error boundaries must be class components — they're the one remaining use case for class components in modern React. You implement componentDidCatch and getDerivedStateFromError:
import { Component } from "react";
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Called during render — update state to show fallback UI
return { hasError: true, error };
}
componentDidCatch(error, info) {
// Called after render — use for logging
console.error("Error caught by boundary:", error);
console.error("Component stack:", info.componentStack);
// logErrorToService(error, info); // e.g. Sentry
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div style={{ padding: "20px", color: "red" }}>
<h2>Something went wrong.</h2>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Two lifecycle methods:
getDerivedStateFromError— runs during the render phase. Return new state to trigger the fallback UI.componentDidCatch— runs after render. Use this to log the error to a monitoring service.
Using Error Boundaries
Wrap any part of your tree to isolate errors:
import ErrorBoundary from "./components/ErrorBoundary";
function App() {
return (
<div>
<Header />
<ErrorBoundary>
<MainContent />
</ErrorBoundary>
<Footer />
</div>
);
}
Now if MainContent or anything inside it throws, the header and footer remain intact — only the bounded section shows the fallback.
Custom Fallback UI
Pass a fallback prop for custom error UI:
<ErrorBoundary
fallback={
<div className="error-card">
<h3>Failed to load this section</h3>
<p>Please refresh the page or try again later.</p>
</div>
}
>
<UserDashboard />
</ErrorBoundary>
Granular Error Isolation
Place multiple boundaries at different levels for different fallback experiences:
function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
<Header />
<main>
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<MainFeed />
</ErrorBoundary>
</main>
<Footer />
</ErrorBoundary>
);
}
A crash in Sidebar shows <SidebarError /> but MainFeed keeps working.
What Error Boundaries Don't Catch
Error boundaries only catch errors that happen during rendering, lifecycle methods, and constructors of child components. They do not catch:
| Not caught | Use instead |
|---|---|
Event handlers (onClick, etc.) | try/catch inside the handler |
Async code (setTimeout, fetch) | try/catch inside async functions |
| Errors in the boundary itself | A parent boundary |
| Server-side rendering errors | SSR-specific handling |
// This is NOT caught by an error boundary
function Button() {
const handleClick = () => {
throw new Error("This won't be caught!"); // use try/catch here
};
return <button onClick={handleClick}>Click</button>;
}
// This IS caught by an error boundary
function BrokenComponent() {
throw new Error("This is caught during render!"); // throws during render
return <div>Never shown</div>;
}
Resetting After an Error
A common pattern — let users retry after an error:
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logError(error, info);
}
reset = () => this.setState({ hasError: false });
render() {
if (this.state.hasError) {
return (
<div>
<p>Something went wrong.</p>
<button onClick={this.reset}>Try again</button>
</div>
);
}
return this.props.children;
}
}
react-error-boundaryThe community package react-error-boundary provides a well-tested, flexible ErrorBoundary component with hooks support, so you don't have to write the class component yourself:
npm install react-error-boundary
import { ErrorBoundary } from "react-error-boundary";
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { /* reset state */ }}>
<YourComponent />
</ErrorBoundary>
Where to Place Error Boundaries
A good rule of thumb: wrap any section that loads async data or renders user-generated content independently:
// Route-level — each page is isolated
<ErrorBoundary><DashboardPage /></ErrorBoundary>
<ErrorBoundary><ProfilePage /></ErrorBoundary>
// Widget-level — widgets can fail independently
<ErrorBoundary><WeatherWidget /></ErrorBoundary>
<ErrorBoundary><StockTicker /></ErrorBoundary>
// Data-fetching components — API failures stay contained
<ErrorBoundary><UserComments postId={id} /></ErrorBoundary>
Don't wrap every single component — that's overkill and clutters the tree. Wrap logical sections where an isolated failure is acceptable.