Error Boundaries
By default, if a component throws an error during rendering, the entire React tree unmounts and the user sees a blank page. Error boundaries are React components that catch JavaScript errors anywhere in their child component tree and display a fallback UI instead of crashing the whole application.
Only Class Components Can Be Error Boundaries
This is one of the few remaining reasons to write a class component in modern React. Error boundaries require two lifecycle methods that have no function-component equivalent (as of React 18):
static getDerivedStateFromError(error)— called during the render phase when a child throws; return new state to trigger the fallback UIcomponentDidCatch(error, info)— called after the render phase; used for logging errors to an error reporting service (Sentry, Datadog, etc.)
Complete Error Boundary Implementation
import React from 'react'
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
// Invoked during render when a descendant throws
// Return value merges into state
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
// Invoked after render — ideal for logging
componentDidCatch(error, errorInfo) {
// errorInfo.componentStack is the React component stack trace
console.error('Uncaught error:', error, errorInfo)
// Send to an error-tracking service:
// reportError({ error, componentStack: errorInfo.componentStack })
}
render() {
if (this.state.hasError) {
// Render fallback UI — can be a custom prop or a default
if (this.props.fallback) {
return this.props.fallback
}
return (
<div role="alert" style={{ padding: '1rem', border: '1px solid red' }}>
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error?.message}
</details>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
)
}
return this.props.children
}
}
export default ErrorBoundaryPlacement Strategy
Where you place an error boundary determines how much of the UI survives an error. Wrapping the entire app in one boundary is the minimum — but it means a single broken widget takes down everything. Granular boundaries give users a better experience.
// ✗ Coarse — any error kills the whole page
<ErrorBoundary>
<App />
</ErrorBoundary>
// ✓ Granular — each section degrades independently
function Dashboard() {
return (
<div>
{/* Sidebar error won't affect the main feed */}
<ErrorBoundary fallback={<p>Sidebar unavailable.</p>}>
<Sidebar />
</ErrorBoundary>
{/* Each widget is isolated */}
<ErrorBoundary fallback={<WidgetError name="Analytics" />}>
<AnalyticsWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="Activity Feed" />}>
<ActivityFeed />
</ErrorBoundary>
</div>
)
}The react-error-boundary Library
Writing a class component every time is tedious. The react-error-boundary package wraps the complexity in a clean functional API:
npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'
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) => console.error(error, info)}
onReset={() => {
// Optional: reset app state before re-rendering children
queryClient.clear()
}}
>
<ComplexFeature />
</ErrorBoundary>
)
}useErrorBoundary Hook (React 19+)
React 19 introduces useErrorBoundary, which lets function components imperatively throw errors into the nearest error boundary — useful for async operations that can't be caught by the boundary's render-phase detection:
import { useErrorBoundary } from 'react'
function UserProfile({ userId }) {
const { showBoundary } = useErrorBoundary()
async function loadUser() {
try {
const data = await fetchUser(userId)
setUser(data)
} catch (err) {
// Throw the async error into the nearest error boundary
showBoundary(err)
}
}
// ...
}
// react-error-boundary also provides this hook for React 18:
import { useErrorBoundary } from 'react-error-boundary'What Error Boundaries Do NOT Catch
// ✗ Error boundary will NOT catch this
function Button() {
function handleClick() {
throw new Error('Event handler error') // NOT caught by boundary
}
return <button onClick={handleClick}>Click me</button>
}
// ✓ Use try/catch for event handler errors
function Button() {
function handleClick() {
try {
doSomethingRisky()
} catch (err) {
setError(err.message) // handle locally
}
}
return <button onClick={handleClick}>Click me</button>
}
// ✗ Error boundary will NOT catch async errors either
function DataFetcher() {
React.useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data))
.catch(err => {
// This catch is needed — boundary won't help here
setError(err)
})
}, [])
}