React Error Boundaries: Don't Let One Bug Crash Everything
Production Ready — Part 5 of 30
The 3 AM Wake-Up Call
It was a Friday evening deploy. The team shipped a new analytics widget on the main dashboard. Everything looked fine in staging. Then, around midnight, Slack lit up: users were seeing a completely blank screen — not just the analytics panel, but the entire app.
The culprit? One line deep inside the new widget was calling .toFixed(2) on a value that occasionally came back as null from the API. That threw a TypeError. React caught the error during rendering, had no idea what to do with it, and unmounted the entire component tree.
Blank screen. Zero UI. Every user logged out and staring at nothing.
This is not a hypothetical. A July 2025 Stack Overflow thread documented exactly this pattern: a corrupted local cache caused LastSeenDateComponent to throw an Uncaught RangeError: Invalid time value, bringing down the whole app. The developer's shock — "it should only affect the one component" — is something I hear constantly from vibe coders. This is where Error Boundaries come in.
What React Does Without Error Boundaries
Here's the brutal truth baked into React since version 16: any uncaught render error unmounts your entire component tree.
The React team made this intentional call. Their reasoning: a corrupted, half-rendered UI (like a payments screen showing a wrong amount) is worse than a blank screen. But "blank screen" is not the same as "good UX." It's just the lesser of two disasters — unless you take control.
Without protection, one bad prop, one undefined where you expected a string, one third-party component with a bug, and your users are staring at nothing.
Error Boundaries: The try/catch of Your Component Tree
An Error Boundary is a React class component that wraps part of your UI. If anything inside it throws during rendering, React calls the boundary instead of unmounting the whole tree. The boundary renders a fallback UI. Everything outside the boundary keeps working.
Here's the minimal implementation:
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Flip the flag so we render the fallback on next render
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// This is your moment to log to Sentry, Datadog, or your own endpoint
console.error('Boundary caught:', error, errorInfo.componentStack);
}
render() {
if (this.state.hasError) {
return <div role="alert">Something went wrong. Try refreshing.</div>;
}
return this.props.children;
}
}
export default ErrorBoundary;
Usage is a one-liner wrap:
<ErrorBoundary>
<AnalyticsWidget />
</ErrorBoundary>
Now when AnalyticsWidget throws, the boundary shows the fallback. Your sidebar, navbar, and the rest of the dashboard? Still running.
What Error Boundaries Do NOT Catch
This is where most vibe coders get tripped up. Error Boundaries only catch errors thrown during rendering and in lifecycle methods. They will not catch:
- Event handlers — use
try/catchinside onClick/onChange - Async code —
fetch,setTimeout, promises - Server-side rendering errors
- Errors thrown inside the boundary itself
If you do await fetch('/api/data') in a useEffect and it throws, the boundary will not catch that. You need explicit try/catch in your effect or a library that bridges async errors to the boundary.
The Right Tool: react-error-boundary
Writing class components in 2025 feels like putting on a tuxedo to check your mail. The react-error-boundary library by Brian Vaughn gives you a functional API that covers the same ground — and handles async errors through the useErrorBoundary hook.
Install it:
npm install react-error-boundary
As of v6.0.0 (released May 2025), the package is ESM-only to align with modern tooling. If you're still on CommonJS builds, you'll need to handle this in your bundler config.
Here's the idiomatic usage with a recovery button:
import { ErrorBoundary } from 'react-error-boundary';
function WidgetFallback({ error, resetErrorBoundary }) {
return (
<div role="alert" className="error-fallback">
<p>Failed to load widget: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function Dashboard() {
return (
<div className="dashboard">
<Sidebar />
<ErrorBoundary
FallbackComponent={WidgetFallback}
onError={(error, info) => logToSentry(error, info)}
onReset={() => queryClient.invalidateQueries(['analytics'])}
>
<AnalyticsWidget />
</ErrorBoundary>
</div>
);
}
The onReset prop is powerful — wire it to invalidate your query cache, reset local state, or anything else needed to recover cleanly.
For async errors in event handlers, use useErrorBoundary:
import { useErrorBoundary } from 'react-error-boundary';
function SaveButton() {
const { showBoundary } = useErrorBoundary();
const handleSave = async () => {
try {
await saveData();
} catch (error) {
showBoundary(error); // Escalates to the nearest ErrorBoundary
}
};
return <button onClick={handleSave}>Save</button>;
}
React 19: Error Boundaries Get a Upgrade
In December 2024, React 19 shipped with a significant improvement to error handling. Previously, a single caught error would log three separate entries to the console — a maddening experience when debugging production issues.
React 19 now logs a single consolidated error with all the relevant context. It also introduced two new root-level hooks:
onCaughtError— fires when React catches an error in an Error BoundaryonUncaughtError— fires when an error throws and no boundary catches it
Here's how to wire both up with Sentry:
import * as Sentry from '@sentry/react';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'), {
onUncaughtError: Sentry.reactErrorHandler(),
onCaughtError: Sentry.reactErrorHandler(),
onRecoverableError: Sentry.reactErrorHandler(),
});
root.render(<App />);
The distinction matters: onCaughtError tells you about errors your boundaries handled gracefully — these are known failure zones. onUncaughtError tells you about catastrophic failures that no boundary stopped — these need immediate attention.
In React 19+, the Sentry docs recommend treating these as complementary layers: reactErrorHandler for global telemetry, <ErrorBoundary> components for scoped fallback UI.
Where to Place Your Boundaries
This is the strategic decision that separates production-grade apps from fragile ones. Think in three layers:
1. Route level — Wrap each page/route. A broken settings page shouldn't kill the billing page.
<Routes>
<Route path="/dashboard" element={
<ErrorBoundary FallbackComponent={PageError}>
<DashboardPage />
</ErrorBoundary>
} />
<Route path="/settings" element={
<ErrorBoundary FallbackComponent={PageError}>
<SettingsPage />
</ErrorBoundary>
} />
</Routes>
2. Feature level — Wrap independent widgets, data panels, and third-party embeds individually. This is exactly what Facebook Messenger does — sidebar, info panel, conversation log, and message input each have their own boundary.
3. Third-party components — Always wrap these. You have no control over their code. A map component, a chart library, a rich text editor — any of these can throw on bad data.
The Production Checklist
Before your next deploy, verify every item:
- Route-level boundaries — Every top-level route is wrapped with an
ErrorBoundary - Widget-level boundaries — Each independently-failable component (charts, feeds, maps) has its own boundary
- Third-party wraps — Every external component library is wrapped
-
onErrorlogging — All boundaries call a real error reporting service (Sentry, Datadog, etc.) incomponentDidCatchor theonErrorprop - Meaningful fallback UI — Fallbacks show actionable messages, not just "Something went wrong"
- Reset capability — Use
onResetto clear stale state so "Try again" actually works - Async error escalation — Event handlers and effects that can fail use
useErrorBoundaryorshowBoundary - React 19 root hooks — If on React 19+, wire
onUncaughtErrorandonCaughtErrorto your monitoring stack - ESM compatibility — If using
react-error-boundaryv6, verify your bundler handles ESM-only packages - Tested under failure — Deliberately throw errors in dev to confirm fallback UI renders as expected
Ask The Guild
This week's community prompt:
What's the most unexpected component crash you've seen take down a production React app? Was it a third-party library, bad API data, or something else entirely? How did you scope your error boundaries after the incident?
Share your war story in the guild — the best ones become case studies for future articles.
Tom Hundley is a software architect with 25 years of experience. He writes the Production Ready series for the AI Coding Guild.