React Error Boundary Patterns and Recovery Strategies
Master error boundary patterns in React including granular boundaries, recovery strategies, logging, and the react-error-boundary library.
What you'll learn
- ✓How error boundaries work in React
- ✓Granular boundary placement strategies
- ✓Recovery and retry patterns
- ✓Using react-error-boundary for cleaner code
- ✓Logging errors to external services
Prerequisites
- •Basic React knowledge
- •Familiarity with component composition
Error boundaries catch JavaScript errors during rendering, in lifecycle methods, and in constructors of the component tree below them. Without boundaries, a single error in one component crashes the entire app. With well-placed boundaries, the rest of the application keeps working while the broken section shows a fallback.
The Class-Based Error Boundary
React only supports error boundaries as class components. You need to implement static getDerivedStateFromError to update state and componentDidCatch to perform side effects like logging.
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <p>Something went wrong.</p>;
}
return this.props.children;
}
}
Use it by wrapping any part of your tree:
<ErrorBoundary fallback={<p>This widget failed to load.</p>}>
<WeatherWidget />
</ErrorBoundary>
What Error Boundaries Do Not Catch
Error boundaries do not catch errors in event handlers, async code (setTimeout, fetch), or server-side rendering. For event handlers, use regular try-catch blocks. For async errors, catch them where the promise is handled or use Suspense with error boundaries.
function SubmitButton() {
const handleClick = async () => {
try {
await submitForm();
} catch (err) {
// Handle async error here, not in a boundary
showToast('Submission failed');
}
};
return <button onClick={handleClick}>Submit</button>;
}
Granular Boundary Placement
The most important decision is where to place boundaries. Too few and a single error takes down a large section. Too many and you litter your code with fallback UI that fragments the user experience.
A good rule of thumb is to place boundaries around independent features. A dashboard might have separate boundaries for the header, each widget, and the sidebar.
function Dashboard() {
return (
<div className="dashboard">
<ErrorBoundary fallback={<HeaderFallback />}>
<Header />
</ErrorBoundary>
<div className="widgets">
<ErrorBoundary fallback={<WidgetFallback name="Sales" />}>
<SalesWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetFallback name="Users" />}>
<UsersWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetFallback name="Revenue" />}>
<RevenueWidget />
</ErrorBoundary>
</div>
<ErrorBoundary fallback={<SidebarFallback />}>
<Sidebar />
</ErrorBoundary>
</div>
);
}
If the SalesWidget crashes, the other widgets and the sidebar remain functional.
Recovery with Key-Based Remounting
The simplest recovery strategy is to remount the failed component. Change the key on the error boundary to force React to unmount and remount the entire subtree, which clears the error state.
function ResettableWidget() {
const [resetKey, setResetKey] = useState(0);
return (
<ErrorBoundary
key={resetKey}
fallback={
<div>
<p>Widget crashed.</p>
<button onClick={() => setResetKey(k => k + 1)}>Retry</button>
</div>
}
>
<ExpensiveWidget />
</ErrorBoundary>
);
}
Clicking Retry increments the key, which unmounts the boundary and its children, creating a fresh instance. This works well when the error was caused by transient state.
Using react-error-boundary
The react-error-boundary library provides a feature-rich boundary component with built-in reset, fallback render props, and error handler callbacks.
npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert" className="error-panel">
<h3>Something went wrong</h3>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
logErrorToService(error, info.componentStack);
}}
onReset={() => {
// Clear any stale state before retry
queryClient.clear();
}}
>
<MainContent />
</ErrorBoundary>
);
}
The onReset callback runs before the boundary re-renders its children, giving you a chance to clear caches or reset state that caused the error.
Resetting on Navigation
When the user navigates to a different page, you typically want to clear any error states. Pass the current route as a resetKey so the boundary resets automatically.
import { useLocation } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
function AppShell() {
const location = useLocation();
return (
<ErrorBoundary
FallbackComponent={PageErrorFallback}
resetKeys={[location.pathname]}
>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</ErrorBoundary>
);
}
Logging Errors to External Services
In production, you need to send errors to a monitoring service like Sentry, Datadog, or LogRocket. The componentDidCatch method (or onError in react-error-boundary) is the right place.
import * as Sentry from '@sentry/react';
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
Sentry.captureException(error, {
extra: { componentStack: info.componentStack },
});
}}
>
<MainApp />
</ErrorBoundary>
);
}
Sentry also provides its own Sentry.ErrorBoundary component that handles this automatically.
Combining with Suspense
Error boundaries and Suspense boundaries work together. Place the error boundary outside the Suspense boundary so it catches both render errors and rejected promises from the use() hook.
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Spinner />}>
<DataComponent dataPromise={dataPromise} />
</Suspense>
</ErrorBoundary>
If dataPromise rejects, the error boundary catches it. If the component throws during render, the error boundary catches that too. The Suspense boundary only handles the pending state.
Building a Reusable Boundary Component
For consistency across your app, create a configured boundary that includes your standard error UI and logging.
import { ErrorBoundary } from 'react-error-boundary';
import { logError } from '@/lib/monitoring';
export function AppErrorBoundary({ children, level = 'section' }) {
return (
<ErrorBoundary
FallbackComponent={level === 'page' ? PageFallback : SectionFallback}
onError={logError}
>
{children}
</ErrorBoundary>
);
}
Then use it consistently:
<AppErrorBoundary level="page">
<Dashboard />
</AppErrorBoundary>
Error boundaries are a safety net. The goal is never to need them, but when something unexpected happens, they keep your app usable and give you the information you need to fix the problem.
Related articles
- React React Custom Hooks: Patterns for Reusable Logic
Master React custom hooks with practical patterns for data fetching, form handling, debouncing, and more. Learn when and how to extract reusable logic.
- React React Compound Components: Flexible APIs Without Prop Drilling
Build flexible, composable React components using the compound components pattern with Context, implicit state sharing, and controlled inversion.
- React React Error Boundaries in Production: Recovery and Monitoring
Production-grade Error Boundary patterns for React apps including granular placement, retry logic, error reporting, and integration with Suspense and routing.
- React React Higher-Order Components vs Hooks: Migration Guide
Compare Higher-Order Components and hooks in React with side-by-side examples, migration strategies, and guidance on when each pattern still makes sense.