Chapter 2 of 4

What Boundaries Catch

The four categories they miss, and what to do about each.

A boundary catches errors thrown during rendering, in lifecycle methods, and in constructors of the components below it. Everything else escapes.

Not caught: event handlers

Handlers run outside the render cycle, so React cannot intercept them. Use a normal try/catch.

function SaveButton() {
  const [error, setError] = useState(null);

  async function handleClick() {
    try {
      await save();
    } catch (err) {
      setError(err);
    }
  }

  if (error) return <p role="alert">Save failed.</p>;
  return <button onClick={handleClick}>Save</button>;
}

Not caught: asynchronous code

A rejected promise or an error inside setTimeout happens long after rendering finished. To route it to a boundary, catch it and set state that throws during the next render.

const [error, setError] = useState(null);
if (error) throw error;   // now a boundary can catch it

useEffect(() => {
  fetchData().catch(setError);
}, []);

Not caught: server rendering

Boundaries do not catch errors during server-side rendering. Frameworks provide their own error handling for that phase - error.js in Next.js, for example.

Not caught: errors in the boundary itself

A boundary cannot catch its own errors. If the fallback throws, the error propagates to the next boundary up - so keep fallbacks extremely simple.

// Risky: error.response may be undefined
<p>{error.response.data.message}</p>

// Safe
<p>Something went wrong. Please try again.</p>