React Error Boundaries in Practice
Where to place error boundaries in a React app, what to show users when something breaks, and how to wire error reporting so you actually hear about fai...
Error boundaries are one of those React features that most apps underuse until a production incident makes the case for them. A single uncaught render error takes down the whole tree. A well-placed boundary contains the damage and keeps the rest of the page alive.
What Boundaries Actually Catch
The important caveat first: error boundaries only catch errors that happen during rendering, in lifecycle methods, and in constructor calls of child components. They do not catch errors in event handlers, async code, or server-side rendering.
That means this will be caught:
function BrokenComponent() {
throw new Error('Something went wrong during render')
return <div>Never reached</div>
}
And this will not:
function BrokenButton() {
const handleClick = () => {
throw new Error('This error is invisible to boundaries')
}
return <button onClick={handleClick}>Click me</button>
}
For async errors and event handler errors you still need try/catch or a global window.onerror handler.
The Class-Based Boundary
React requires class components for error boundaries. There is no hook equivalent. Here is a typed boundary you can drop into any project:
import { Component, type ErrorInfo, type ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
onError?: (error: Error, info: ErrorInfo) => void
}
interface State {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('[ErrorBoundary]', error, info.componentStack)
this.props.onError?.(error, info)
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <p>Something went wrong.</p>
}
return this.props.children
}
}
getDerivedStateFromError flips the error state so the fallback renders. componentDidCatch is where you log and report. The two methods serve different purposes, so keep them separate.
A Functional Wrapper with react-error-boundary
If you would rather avoid writing class components yourself, the react-error-boundary package is a solid choice. It wraps the class boundary in a clean API with reset support built in:
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error
resetErrorBoundary: () => void
}) {
return (
<div role="alert">
<p>Something went wrong: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
export function WidgetSection() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
reportError(error, info.componentStack)
}}
>
<SomeWidget />
</ErrorBoundary>
)
}
The resetErrorBoundary callback clears the error state and tries to remount the children. For widgets that fetch data this can be enough to recover without a full page reload.
Where to Place Boundaries
Placement is a design decision, not a technical one. A single boundary at the app root keeps the UI from going completely blank, but it also means one broken widget takes down everything inside it.
A better default is one boundary per logical region:
- Page level catches errors in top-level route components and shows a full-page error screen.
- Widget or section level isolates sidebars, feeds, recommendation panels, or any feature that is secondary to the main content. If the recommendation widget breaks, the article should still load.
- Third-party boundaries wrap any component that reaches outside your codebase, such as embedded analytics, chat widgets, or library components you do not fully control.
The practical test: if this component threw during render, which parts of the page should still work? Wrap the parts that should not drag each other down.
Fallback UX
What you show in the fallback matters. A blank space is confusing. A message that says "Sorry, an error occurred" with no further action is a dead end.
Good fallbacks:
- Acknowledge that something broke, briefly
- Offer a recovery action when one exists (try again, refresh)
- Show a reduced version of the feature if you can, for example a static snapshot instead of a live chart
Avoid showing technical error messages to end users. Log the details, show a human summary.
Wiring Error Reporting
componentDidCatch fires on every caught error. That is the right place to send data to an error tracking service such as Sentry, Datadog, or a plain HTTP endpoint.
function reportError(error: Error, componentStack: string | null | undefined) {
// Replace with your actual reporting service
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: error.message,
stack: error.stack,
componentStack,
timestamp: new Date().toISOString(),
}),
}).catch(() => {
// Reporting itself should never throw
})
}
Keep the reporting function outside the component so you can reuse it across multiple boundaries. Always swallow errors inside the reporter, because a failure in your error reporting path should not create a second failure.
The Minimal Setup
If you are adding boundaries to an existing app today, start with two:
- One at the top of your root layout, so the page never goes fully blank.
- One around anything that is optional or feature-specific, so failures there do not break the core experience.
Add more as you see errors in production. The boundary placements will tell you where the real failure points are.
Newsletter
A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.