Which failures does a React error boundary NOT catch, and how do you get those errors in front of a boundary anyway?
answer
- React must be on the stack
- handlers and timers run without React
- effects are caught, clicks are not
- catch it, store it, throw while rendering
- boundary destroys state — prefer inline errors
basics
~20 sReact error boundaries only catch errors React itself throws while rendering, in constructors, in lifecycle methods, and in effects. They miss event handlers, asynchronous callbacks, server rendering, and the boundary's own errors. The fix is to catch such errors and rethrow them during render.
solid answer
~50 sA boundary wraps React's own call stack, so it catches renders, constructors, lifecycle methods, and effect bodies and cleanups. It misses anything React did not call as part of that work: an `onClick` handler, a `setTimeout` or `.then` callback, an `async` function body after its first `await`, errors thrown during server rendering (there is no commit on the server), and errors thrown by the boundary itself, which propagate to the next boundary up. The standard workaround is to convert the escaped error into a render-time one: catch it, put it in state, and `throw` it from the component body on the next render — which is precisely what the `useErrorBoundary()` hook's `showBoundary` function from the `react-error-boundary` package does for you. One async case needs no workaround: a promise read with `use()` that rejects throws during render, so the nearest boundary handles it normally.
code
jsx · 18 linesimport { useState } from 'react';
function SaveButton({ save }) {
const [error, setError] = useState(null);
// Rethrown during render, so the nearest error boundary catches it.
if (error) throw error;
async function handleClick() {
try {
await save();
} catch (e) {
setError(e);
}
}
return <button onClick={handleClick}>Save</button>;
}go deeper
Memorise the exclusion list — event handlers, asynchronous callbacks, server rendering, and the boundary's own errors — and know that effects and rendering are on the caught side.
Give the rule that produces the list: React must be on the call stack for the boundary to catch. Then show the rethrow-during-render workaround that turns a handler or async error into one a boundary sees.
Add the judgment call: escalating an expected fetch failure to a boundary wipes out the user's in-progress work, so route those to inline UI and reserve boundaries for genuine bugs, backed by global error and unhandledrejection listeners.
Own the error taxonomy across the app — which failures are modelled state, which are boundary-worthy, what each tier reports — so telemetry distinguishes handled product errors from real defects instead of merging them into one noisy stream.
## The rule behind the list Most candidates memorise "boundaries don't catch event handlers and async code" without the rule that generates it. The rule is simple: **a boundary catches only errors that unwind through React's own stack frames.** React calls your component to render, calls a constructor, calls a lifecycle method, calls an effect and its cleanup — in all of those, React is on the stack and can `catch`. When the browser calls your code directly, React is not there. ## What is caught - rendering (a function component's body, a class `render`); - class constructors; - lifecycle methods; - `useEffect` / `useLayoutEffect` bodies and their cleanup functions; - a promise read with `use()` that rejects — the rejection is thrown out of the render, so it reaches a boundary like any render error. ## What is not caught, and why **Event handlers.** The browser dispatches the event; React's dispatch code runs your handler, but a throw there is not a render failure and React lets it reach the global `error` handler. Nothing about the committed tree is invalid, so there is nothing for React to tear down. **Asynchronous callbacks.** `setTimeout`, `requestAnimationFrame`, `.then`, the body of an `async` function after its first `await` — all of these run in a fresh task or microtask with an empty stack. React is long gone. **Server rendering.** Producing HTML on the server never commits, so there is no fallback to swap in and `componentDidCatch` never runs. The server rendering API reports the failure through its own error callbacks; for content inside a `<Suspense>` boundary, React can discard that part of the stream and retry it on the client, where a real boundary is in play. **The boundary's own errors.** A boundary cannot catch itself, by the same logic as a `catch` block that throws. The error goes to the next boundary above, so keep fallback UI trivial — no data access, no hooks that can fail. **Errors after unmount.** A callback that fires for a subtree the boundary already replaced has nothing to unwind into. ## Turning an escaped error into a caught one The generic technique is to move the failure onto the render path. Catch it where it happens, store it in state, and throw it from the component body on the next render: ```jsx function SaveButton({ save }) { const [error, setError] = useState(null); if (error) throw error; // now a render error return ( <button onClick={async () => { try { await save(); } catch (e) { setError(e); // schedules the throwing render } }}>Save</button> ); } ``` The `if (error) throw error;` line is the whole trick: `setError` schedules a render, and on that render React is back on the stack, so the nearest boundary catches. The `react-error-boundary` package packages this as `const { showBoundary } = useErrorBoundary()` — calling `showBoundary(e)` pushes any error into the enclosing boundary from a handler or an async callback. ## When *not* to reach for that trick Escalating every failed request to a boundary is usually the wrong product decision. A boundary destroys the subtree: the user's typing, scroll position, and open menus all go with it. A failed "Save" is better handled by rendering an inline error message and leaving the form intact; the boundary is for failures the component cannot represent at all — corrupt data it cannot render, a missing invariant, a bug. A useful split: - **Expected failures** (validation rejected, network timeout, 404) → local state, inline UI, retry button. These are part of the component's contract. - **Unexpected failures** (a bug, an impossible state, a rejected `use()` promise nobody modelled) → boundary. ## Global handlers still matter Because handler and async errors bypass boundaries entirely, an app that only wires boundaries has blind spots in its telemetry. Pair them with `window.addEventListener('error', ...)` and `window.addEventListener('unhandledrejection', ...)` so escaped errors are still reported, even when they never produce a fallback. ## What a strong answer sounds like State the rule (React must be on the stack), give the list as a consequence rather than a memorised set, name the rethrow-during-render workaround, and then add the judgment: most handler and fetch failures should be shown inline, not escalated to a boundary that wipes out the user's work.
- An error thrown inside a useEffect body — caught or not?Caught. React invokes effects as part of the commit, so the throw unwinds through React's own frames and reaches the nearest boundary. The same is true of an effect's cleanup function. What escapes is the callback the effect *schedules* — a timer or a promise continuation — because that runs later with no React frame on the stack.
- Why is 'push every failed fetch into a boundary' usually a bad idea?Because a boundary unmounts its whole subtree: half-typed forms, scroll position and open dialogs go with it. Expected failures such as a timeout or a rejected save belong in local state with inline messaging and a retry. Reserve the boundary for failures the component genuinely cannot render around.
- How does a rejected promise read with use() interact with boundaries?It reaches them normally. `use(promise)` suspends while the promise is pending and throws the rejection during render when it settles, so the nearest error boundary shows a fallback and the nearest `<Suspense>` shows the pending state. That is the one async path where no rethrow trick is needed.
saying these in an interview costs you the question
- Claims boundaries catch any error anywhere in the subtree
- Thinks async errors are caught if the call started during render
- Says errors in useEffect escape like event handler errors
- Expects componentDidCatch to fire during server rendering
- Wraps every failed request in a boundary instead of inline UI