Why Error Boundaries Do Not Catch Every Error

Error boundaries only catch render-time errors in child components. Event handlers, async code, server rendering, and the boundary itself stay outside their reach.

6 min read

Error boundaries catch render-time errors in child components, but they do not catch every error a React app can throw. Knowing the gaps tells you where to add try/catch, promise handling, or server-side checks instead of relying on a boundary.

What boundaries actually cover

A boundary catches an error thrown while React renders a child component. It swaps in a fallback and keeps the rest of the tree alive, so one broken widget does not take down the page. The mechanism is the same class you build once in React error boundaries explained with a reusable example.

The four cases they miss

Error sourceWhy a boundary misses itWhat to use instead
Event handlershandlers run outside rendertry/catch in the handler
Async codetimers and promises resolve latera catch and error state
Server renderingno boundary mounts on the servertry/catch in server code
The boundary itselfit cannot catch its own errora parent boundary or top-level handler

Event handlers, timers, and promises all run after render finishes, so no boundary is watching. Server rendering happens before any boundary mounts in the browser. A boundary that throws cannot catch itself.

Event handler errors

A click handler that throws does not trigger the nearest boundary. Catch it at the call site and turn it into visible UI.

App.jsxApp.jsx
import { useState } from "react";
 
export default function SaveButton({ onSave }) {
  const [error, setError] = useState("");
  async function handleClick() {
    setError("");
    try {
      await onSave();
    } catch {
      setError("Saving failed. Try again.");
    }
  }
  return (
    <>
      <button onClick={handleClick}>Save</button>
      {error && <p role="alert">{error}</p>}
    </>
  );
}

When onSave rejects, the catch sets a message and the paragraph appears. The boundary above never sees the error because the rejection happened in an async handler, not during render.

Async errors

A rejected fetch or a throwing timer callback also misses boundaries. Attach a catch and store the failure in state, then render an error state.

Keep the loading and error branches separate so the user sees which one happened. This is the pattern from how to handle loading, error, empty, and success states.

The exception: transitions

Errors thrown inside the function you pass to startTransition are caught by error boundaries. Marking an update as a transition lets React retry it, and a boundary can catch the error from that retried render. This is the one async-shaped case where a boundary still applies.

What to learn next

For the render errors a boundary does catch, isolate the throwing component before you guard it. Naming the phase first keeps that isolation quick.

Rune AI

Rune AI

Key Insights

  • Boundaries catch only render-time errors in children.
  • Event handlers need try/catch at the call site.
  • Async code needs a catch and an error state.
  • Server rendering needs try/catch in server code.
  • Errors inside startTransition are still caught.
RunePowered by Rune AI

Frequently Asked Questions

Does an error boundary catch a rejected promise?

No. A promise rejection happens after render, so no boundary is watching. Attach a catch and store the failure in state.

What catches event handler errors?

A try/catch block around the handler body. A global window error listener can log the rest as a last resort.

Conclusion

Error boundaries catch only render-time errors in children. Event handlers, async work, server rendering, and the boundary's own errors stay outside, so each needs its own handling.