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.