React Error Boundaries: Graceful Error Handling (2026)
Without error boundaries, an unhandled JavaScript error inside a React component tree unmounts the entire app and shows a blank screen. Error boundaries catch rendering errors in their subtree, log them, and display a fallback UI — keeping the rest of the app functional. This guide covers everything from class-based boundaries to the modern react-error-boundary library, reset strategies and async error handling.
Table of Contents
What Error Boundaries Catch
Error boundaries catch errors during:
- Rendering (including render methods)
- Lifecycle methods
- Constructors of child components
They do not catch:
- Event handlers (use try/catch)
- Async code (setTimeout, fetch callbacks) — unless you rethrow into React's state
- Server-side rendering errors
- Errors in the boundary itself
Class-Based Error Boundaries
Error boundaries must be class components — they're one of the few remaining reasons to write class components:
import { Component } from 'react'
class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
// Called during rendering — update state to show fallback
return { hasError: true, error }
}
componentDidCatch(error, info) {
// Called after render — good for logging
console.error('Error caught by boundary:', error, info.componentStack)
// reportToSentry(error, info)
}
render() {
if (this.state.hasError) {
return this.props.fallback || <h2>Something went wrong.</h2>
}
return this.props.children
}
}
// Usage
<ErrorBoundary fallback={<ErrorPage />}>
<App />
</ErrorBoundary>
react-error-boundary Library
The react-error-boundary package provides a flexible, function-friendly API:
npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'
// Simple fallback component
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{ color: 'red' }}>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => logError(error, info)}
onReset={() => {
// Reset app state here
}}
>
<UserProfile />
</ErrorBoundary>
)
}
Inline fallback with fallbackRender prop:
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<DataWidget />
</ErrorBoundary>
Reset and Recovery Strategies
Use resetKeys to automatically reset the boundary when certain values change:
import { ErrorBoundary } from 'react-error-boundary'
function ProductPage({ productId }) {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
// Reset automatically when productId changes (user navigates to different product)
resetKeys={[productId]}
>
<ProductDetails id={productId} />
</ErrorBoundary>
)
}
Programmatic reset with useErrorBoundary:
'use client'
import { useErrorBoundary } from 'react-error-boundary'
function DataFetcher() {
const { showBoundary } = useErrorBoundary()
async function loadData() {
try {
const data = await fetchData()
setData(data)
} catch (error) {
// Propagate async error to nearest ErrorBoundary
showBoundary(error)
}
}
return <button onClick={loadData}>Load Data</button>
}
Async Errors
Errors in event handlers and async functions don't propagate to error boundaries by default. Two approaches:
// Approach 1: useErrorBoundary hook (react-error-boundary)
import { useErrorBoundary } from 'react-error-boundary'
function AsyncComponent() {
const { showBoundary } = useErrorBoundary()
useEffect(() => {
fetchData()
.then(setData)
.catch(showBoundary) // Routes error to boundary
}, [])
}
// Approach 2: setState trick (works with any error boundary)
function useAsyncError() {
const [, setError] = useState()
return useCallback(
error => setError(() => { throw error }),
[]
)
}
function AsyncComponent() {
const throwError = useAsyncError()
useEffect(() => {
fetchData().catch(throwError)
}, [])
}
Error Boundaries with Suspense
Always pair Suspense with an Error Boundary — a suspended component that rejects its promise needs an error boundary to catch the failure:
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
function SafeDataView() {
return (
<ErrorBoundary
FallbackComponent={({ error, resetErrorBoundary }) => (
<div>
<p>Failed to load: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<Suspense fallback={<Spinner />}>
<DataView />
</Suspense>
</ErrorBoundary>
)
}
Production Error Reporting
import * as Sentry from '@sentry/nextjs'
import { ErrorBoundary } from 'react-error-boundary'
function onError(error, info) {
Sentry.withScope(scope => {
scope.setExtra('componentStack', info.componentStack)
Sentry.captureException(error)
})
}
function App() {
return (
<ErrorBoundary
FallbackComponent={GlobalErrorFallback}
onError={onError}
>
<Router />
</ErrorBoundary>
)
}
Error Boundaries in Next.js
Next.js App Router uses error.tsx files as error boundaries per route segment. These must be Client Components:
// app/dashboard/error.tsx
'use client'
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong in Dashboard</h2>
<button onClick={reset}>Try again</button>
</div>
)
}
// app/global-error.tsx — catches errors in the root layout
'use client'
export default function GlobalError({ error, reset }) {
return (
<html><body>
<h2>Something went wrong</h2>
<button onClick={reset}>Try again</button>
</body></html>
)
}