The Complete Guide to React Error Boundaries

Something will go wrong in your React app. A component will try to read a property on undefined. An API response will have an unexpected shape. A third-party library will throw during render. This is not pessimism — it's the nature of software running in a browser with real data from real users.
The question isn't whether errors happen. It's whether your app handles them gracefully or shows a blank white screen.
Error boundaries are React's answer. Most developers know they exist. Far fewer use them effectively.
What error boundaries catch
Error boundaries catch errors that occur during:
- Rendering — a component throws while React is building the UI
- Lifecycle methods —
componentDidMount,componentDidUpdate - Constructors of child components
They do not catch errors in:
- Event handlers —
onClick,onChange, and friends. Use try/catch there - Async code —
setTimeout,fetch, Promises. These run outside the render cycle - Server-side rendering — boundaries only work in the browser
- The error boundary itself — a boundary can't catch its own errors
This distinction matters. A component that fetches data and throws during the fetch won't be caught by an error boundary — the fetch happens asynchronously. A component that throws while rendering the fetched data will be caught.
The class component reality
Error boundaries must be class components. React has not shipped a hook equivalent — useErrorBoundary doesn't exist in React core. This is a long-standing limitation that the React team has acknowledged but not yet resolved.
Writing a class component today feels like putting on a suit to mow the lawn. But for error boundaries, it's still the only native option:
Two lifecycle methods do the work:
getDerivedStateFromError — automatically called when a child throws. Returns new state that tells the boundary to show the fallback. Static — no access to this.
componentDidCatch — called after getDerivedStateFromError. Receives the error and a component stack trace. This is where you call Sentry, Datadog, or whatever error tracking service you use.
Skip the class component — use react-error-boundary
The react-error-boundary library wraps the class component boilerplate and exposes a clean API that feels like modern React:
The resetErrorBoundary function clears the error state and re-renders the children — no key-changing trick required. onError replaces componentDidCatch. Clean, typed, and no class components in your codebase.
Granular placement: the strategy that matters
Here's the mistake most codebases make. One error boundary wrapping the entire app:
When any component anywhere in the tree throws, the entire app disappears and the user sees a generic error message. They've lost their current context, their scroll position, any unsaved input — everything.
The better strategy: boundaries at the feature level, not the app level.
If AnalyticsWidget throws, the user still sees the activity feed and the revenue chart. The error is contained. The rest of the page is intact.
This is the same philosophy as microservice isolation applied to the component tree: a failure in one subsystem shouldn't take down the whole system.
The reset function and when to use it
React's default behaviour when an error boundary catches an error: show the fallback and stay there. The user has to manually refresh to try again.
react-error-boundary's resetErrorBoundary gives users a retry path without a full page reload. But reset alone isn't always enough — if the error was caused by state that's still wrong, resetting the boundary will just throw again immediately.
The resetKeys prop handles this: when any value in the resetKeys array changes, the boundary resets automatically.
When the user navigates to a different profile, userId changes, the boundary resets, and ProfileContent gets a fresh render with the new userId. No manual retry button needed — the error clears naturally when the context changes.
Logging errors to Sentry
The onError callback (or componentDidCatch in a custom class boundary) is where external error logging goes. With Sentry:
Sentry also ships its own Sentry.ErrorBoundary component that wraps react-error-boundary with automatic capture built in:
The showDialog prop opens Sentry's user feedback dialog when an error is caught — letting users describe what they were doing when it broke. Surprisingly useful for debugging intermittent issues.
Error boundaries with Suspense
Error boundaries and Suspense compose naturally — and in the App Router, they're often used together. Suspense handles the loading state, the error boundary handles the failure state.
The order matters: error boundary wraps Suspense, not the other way around. If the suspended data fetch fails and throws, the error boundary catches it. If Suspense wrapped the boundary, a throw inside Suspense would suspend indefinitely rather than being caught.
Development vs production
In development, React intentionally re-throws errors after an error boundary catches them — so they appear in the browser console and the React error overlay. This can look like the boundary isn't working when it is.
In production, React catches the error, renders the fallback, and stays quiet. The error boundary is doing its job — you just don't see it unless you check your error tracking dashboard.
Don't mistake the development overlay for a boundary failure. Test error boundary behaviour in a production build or with process.env.NODE_ENV === 'production' explicitly set.
The placement checklist
When deciding where to put an error boundary, ask:
If this component throws, what's the minimum UI that should survive?
The answer tells you where the boundary goes. If a sidebar widget throws and the main content should still show — boundary wraps the widget. If an entire route's data fetch fails and the whole page is unusable — boundary wraps the route.
At minimum, every app should have:
- A root-level boundary — catches anything that slips through everything else. Last resort, generic message.
- Route-level boundaries — one per major route. Isolates page-level failures from the navigation chrome.
- Feature-level boundaries — around independent widgets, data-heavy components, and third-party integrations.
Error boundaries aren't glamorous. They're the part of the codebase nobody thinks about until production breaks. But an app with well-placed boundaries fails gracefully — users see a contained error message with a retry option, not a blank screen with no path forward.
That difference is worth the thirty minutes it takes to add them properly.
Up next: Controlled vs uncontrolled components — the decision you keep making wrong — the React form pattern that determines whether you fight your inputs or work with them.
Related: Next.js error handling — error.tsx, global-error.tsx, and when to use each — how error boundaries translate to the App Router's file-based conventions.
Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.
Photo by Stanisław Krawczyk on Unsplash

