Error boundaries work fine in production. The problem is React goes silent in production builds and suddenly you're debugging blind.
Your error boundary catches the error and shows a nice fallback UI. Great. But the actual error? Minified React error #31 or some other cryptic bullshit that tells you nothing. Meanwhile, users are complaining about broken checkouts and your Sentry dashboard shows a grand total of zero useful information.
The Real Problem Nobody Talks About
Here's the uncomfortable truth: error boundaries catch rendering errors, but most production React failures happen outside of rendering. API calls fail in useEffect. Event handlers throw exceptions. Promise rejections go unhandled. Your beautiful error boundary sits there looking pretty while your app dies in a dozen different ways it can't catch.
I spent 6 hours debugging why a dashboard component kept showing white screens in production. The error boundary was perfect - caught errors, showed fallback UI, logged to Sentry. Problem was, the real issue was a failed API call in useEffect that never triggered the error boundary. Users saw "Loading..." forever while I was chasing render errors that didn't exist.
What Error Boundaries Actually Catch (The Short List)
Error boundaries catch exactly three things:
- Render method errors
- Lifecycle method errors (componentDidMount, useEffect callbacks)
- Constructor errors in class components
That's it. Everything else - the stuff that actually breaks in production - you handle yourself.
What they don't catch (the stuff that ruins your weekend):
- Event handler failures (onClick, onSubmit, etc.)
- Async code errors (fetch failures, setTimeout crashes)
- Promise rejections from useEffect
- Custom hook errors outside of rendering
- WebSocket connection failures
- Service worker errors
The react-error-boundary Library: Less Pain, More Function
react-error-boundary by Brian Vaughn saves you from writing class components in 2025. It also gives you the useErrorBoundary
hook, which is the only way to handle async errors properly:
import { useErrorBoundary } from 'react-error-boundary'
function DataComponent() {
const { showBoundary } = useErrorBoundary()
useEffect(() => {
fetch('/api/data')
.then(response => {
if (!response.ok) throw new Error(`API failed: ${response.status}`)
return response.json()
})
.then(setData)
.catch(showBoundary) // Now your error boundary actually catches this
}, [showBoundary])
}
Without showBoundary
, that fetch failure would never trigger your error boundary. Users would see loading spinners forever while your error monitoring shows nothing.
Where to Put Error Boundaries (And Where Not To)
Don't wrap every component in an error boundary. That's not granular error handling - that's paranoia. Focus on blast radius: what's the biggest thing that can fail without taking down unrelated features?
E-commerce example that actually makes sense:
<div className=\"checkout-page\">
{/* Cart crashes shouldn't kill payment form */}
<ErrorBoundary FallbackComponent={CartError}>
<ShoppingCart />
</ErrorBoundary>
{/* Payment crashes shouldn't kill cart */}
<ErrorBoundary FallbackComponent={PaymentError}>
<PaymentForm />
</ErrorBoundary>
</div>
Don't do this bullshit:
<ErrorBoundary>
<ErrorBoundary>
<ErrorBoundary>
<Button onClick={handleClick}>Click</Button>
</ErrorBoundary>
</ErrorBoundary>
</ErrorBoundary>
The Production Debugging Reality
When error boundaries trigger in production, you need real debugging information. React's production builds minify error messages into useless codes. React's error decoder helps, but you're still missing context.
Set up proper error reporting:
import * as Sentry from '@sentry/react'
function ProductionErrorBoundary({ children }) {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
Sentry.captureException(error, {
tags: { section: 'error_boundary' },
contexts: {
react: { componentStack: errorInfo.componentStack }
}
})
}}
>
{children}
</ErrorBoundary>
)
}
Without proper error reporting, error boundaries are just fancy loading states. You catch the error, show a message, and learn nothing about what actually went wrong.
React 19 Error Boundary Changes (December 2024)
React 19 actually improved error boundaries by reducing noise. Previously, every error caused three console messages - the original error, a re-throw after failed recovery, and a final log. Now you get one clean error message.
The bigger change: React 19 broke third-party libraries that access element.ref
directly. Material-UI v4, older React Hook Form versions, and Framer Motion < v11 throw errors that error boundaries handle differently.
React 19 also introduced onUncaughtError
and onRecoverableError
callbacks in createRoot()
for custom error handling. Most apps won't need these, but they're useful if you're building error reporting infrastructure.
Error boundaries are essential, but they're not magic. They catch a specific subset of React errors. Everything else - the API failures, network timeouts, and async disasters that actually break production apps - you handle with try/catch blocks, proper error reporting, and realistic user feedback.
Most importantly: test your error boundaries with realistic production scenarios. Throw errors in useEffect callbacks, simulate network failures, break your API responses. Your local error boundary that catches throw new Error('test')
means nothing if it can't handle the weird shit that happens when real users hit your app.