Question presented to candidate:
"You have an Error Boundary and a component still crashes the page. What kinds of error does it not catch?"
What a strong answer should cover:
The unifying rule: a boundary catches errors thrown while React is on the stack rendering or committing that subtree. Anything thrown outside that window is invisible to it.
Caught: errors during render, during useEffect and layout effects, in constructors and other lifecycle methods.
Not caught: event handlers — React is not rendering when your click handler runs.
Not caught: anything asynchronous — setTimeout, promise callbacks, requestAnimationFrame. The throw happens on a later tick with no React frame below it.
Not caught: errors in server-side rendering, and errors in the boundary's own render — those go to the next boundary up.
The practical consequences: wrap async work in try/catch and put the failure in state; a rejected promise needs .catch or an unhandledrejection handler.
Errors thrown by a suspended promise resolving are surfaced through Suspense and do reach a boundary.
React 19's root-level onUncaughtError and onCaughtError give you the reporting hook for what boundaries do and do not handle.
Clarifying questions expected:
"Where is the throw actually happening — render, an effect, a handler, or a timer?" — that alone answers it.
"Is the failing code a promise rejection?" — that needs a different mechanism entirely.
Code / implementation expected: Optional. A grid of four throw sites with the outcome of each is the most convincing form.
reacterror-handlinglifecycle
02
02
Attempt it yourself
Sketch your approach before reading the solution — that's what interviews test.
Nudge consolestandby
Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.
03
03
Study the solution
Target Audience: Engineers preparing for React interviews — assumes what an Error Boundary is.
Difficulty: Hard
How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview t
Solution ready — 2 min read
Classified // press E to declassify
04
04
Explore the playground snippets
Four throw sites, one boundary — press each and see which are caught
importReact, { Component, useState, useEffect } from"react";
classBoundaryextendsComponent {
state = { error: null };
staticgetDerivedStateFromError(error) { return { error }; }
render() {
if (this.state.error) {
return (
<divstyle={bad}><strongstyle={{color: "#a33" }}>✅ boundary caught it:</strong>{" "}
{this.state.error.message}
<div><buttononClick={() => this.setState({ error: null })}>reset</button></div></div>
);
}
returnthis.props.children;
}
}
functionSubject({ mode, onEscaped }) {
const [asyncError, setAsyncError] = useState(null);
// THE BRIDGE: an error caught asynchronously and re-thrown during render is// back inside the window a boundary can see.if (asyncError) throw asyncError;
// ✅ CAUGHT — render is inside the window.if (mode === "render") thrownewError("thrown during render");
// ✅ CAUGHT — effects run during the commit, with React on the stack.useEffect(() => {
if (mode === "effect") thrownewError("thrown inside useEffect");
}, [mode]);
// ❌ NOT CAUGHT — the timer fires on a later tick with no React frame below.useEffect(() => {
if (mode !== "timer") return;
const t = setTimeout(() => {
try {
thrownewError("thrown inside setTimeout");
} catch (e) {
onEscaped("setTimeout: " + e.message + " — the boundary never saw this");
}
}, 50);
return() =>clearTimeout(t);
}, [mode, onEscaped]);
return (
<divstyle={ok}><div>rendering normally (mode: {mode || "idle"})</div>
{/* ❌ NOT CAUGHT — React has finished rendering by the time this runs. */}
<buttononClick={() => {
try {
throw new Error("thrown in an event handler");
} catch (e) {
onEscaped("handler: " + e.message + " — the boundary never saw this");
}
}}>
throw in a handler
</button>{" "}
<buttononClick={() => {
// The same handler error, BRIDGED into render.
setAsyncError(new Error("handler error, bridged into render"));
}}>
throw in a handler, bridged
</button></div>
);
}
const ok = { border: "1px solid #cde3cd", background: "#f2f9f2", borderRadius: 8, padding: 12 };
const bad = { border: "1px solid #e0b4b4", background: "#fdf0f0", borderRadius: 8, padding: 12 };
exportdefaultfunctionApp() {
const [mode, setMode] = useState("");
const [escaped, setEscaped] = useState([]);
constnote = (s) => setEscaped((l) => [s, ...l].slice(0, 4));
return (
<divstyle={{padding:24, fontFamily: "system-ui", lineHeight:1.6, maxWidth:560 }}><divstyle={{display: "flex", gap:8, flexWrap: "wrap", marginBottom:10 }}><buttononClick={() => setMode("render")}>throw in render</button><buttononClick={() => setMode("effect")}>throw in useEffect</button><buttononClick={() => setMode("timer")}>throw in setTimeout</button><buttononClick={() => { setMode(""); setEscaped([]); }}>reset all</button></div><Boundarykey={mode}><Subjectmode={mode}onEscaped={note} /></Boundary><divstyle={{marginTop:10, fontSize:13 }}><strong>escaped the boundary:</strong><prestyle={{background: "#f6f6f8", padding:8, borderRadius:6, fontSize:12, minHeight:60 }}>
{escaped.length ? escaped.join("\n") : "(nothing yet)"}
</pre></div><pstyle={{fontSize:13, color: "#666" }}>
Render and effect errors reach the boundary. The handler and timer
errors do not — they are caught locally here only so the demo survives;
without that they would go straight to the global handler. The last
button shows the bridge: store the error, throw it during render, and
it becomes catchable.
</p></div>
);
}