The Problem
React Error Boundaries are incredibly useful, but they have a critical limitation: they only catch errors during rendering, in lifecycle methods, and in constructors. They do NOT catch errors in async code, event handlers, setTimeout, or server-side rendering.
Symptoms
- Error Boundary fallback UI never appears for async errors
- Uncaught Promise rejections crash the entire app
- Errors in onClick handlers bypass the Error Boundary entirely
- Users see blank screens with no fallback UI for network failures
What Error Boundaries Do NOT Catch
```javascript class ErrorBoundary extends React.Component { // Only catches: render errors, constructor errors, lifecycle errors static getDerivedStateFromError(error) { return { hasError: true }; } }
// NOT caught: Event handlers function Button() { return <button onClick={() => { throw new Error('Click error'); }}>Click</button>; }
// NOT caught: Async code function AsyncComponent() { useEffect(() => { fetchData().then(data => { throw new Error('Async error'); // Bypasses Error Boundary! }); }, []); return <div>OK</div>; }
// NOT caught: setTimeout function Timer() { useEffect(() => { setTimeout(() => { throw new Error('Timer error'); // Bypasses Error Boundary! }, 1000); }, []); return <div>OK</div>; } ```
How to Fix It
Fix 1: Catch Async Errors Manually
```javascript function AsyncComponent() { const [error, setError] = useState(null); const [data, setData] = useState(null);
useEffect(() => { fetchData() .then(data => setData(data)) .catch(err => setError(err)); // Handle in component }, []);
if (error) return <ErrorFallback error={error} />; if (!data) return <Loading />; return <div>{data.value}</div>; } ```
Fix 2: Wrap Event Handlers with try/catch
```javascript function SafeButton() { const [error, setError] = useState(null);
const handleClick = async () => { try { await doSomethingDangerous(); } catch (err) { setError(err); } };
if (error) return <ErrorFallback error={error} />; return <button onClick={handleClick}>Click</button>; } ```
Fix 3: Global Error Handler as Safety Net
```javascript // In your app entry point window.addEventListener('error', (event) => { console.error('Uncaught error:', event.error); // Log to error tracking service reportError(event.error); });
window.addEventListener('unhandledrejection', (event) => { console.error('Unhandled rejection:', event.reason); reportError(event.reason); }); ```
Fix 4: Use React 19 onError for Server Components
```javascript // Next.js app directory export default function Layout({ children }) { return ( <ErrorBoundary fallback={<GlobalError />}> {children} </ErrorBoundary> ); }
// For async errors in server components, use error.js boundary ```
Pattern: Unified Error Handler
```javascript function useAsyncError() { const [error, setError] = useState(null); const setErrorSafe = useCallback((err) => { // Force React to re-render with error state setError(() => { throw err; }); }, []); return [error, setErrorSafe]; }
function MyComponent() { const [, setError] = useAsyncError();
useEffect(() => { fetchData().catch(setError); // Now caught by Error Boundary! }, []); } ```
This pattern throws the error during render, which the Error Boundary CAN catch.