Skip to solution
hardFrontend

What can and can't an Error Boundary catch?

148 views
01

Understand the problem

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 asynchronoussetTimeout, 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

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

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

Explore the playground snippets

Four throw sites, one boundary — press each and see which are caught
Run Playground
import React, { Component, useState, useEffect } from "react";

class Boundary extends Component {
  state = { error: null };
  static getDerivedStateFromError(error) { return { error }; }
  render() {
    if (this.state.error) {
      return (
        <div style={bad}>
          <strong style={{ color: "#a33" }}>✅ boundary caught it:</strong>{" "}
          {this.state.error.message}
          <div><button onClick={() => this.setState({ error: null })}>reset</button></div>
        </div>
      );
    }
    return this.props.children;
  }
}

function Subject({ 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") throw new Error("thrown during render");

  // ✅ CAUGHT — effects run during the commit, with React on the stack.
  useEffect(() => {
    if (mode === "effect") throw new Error("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 {
        throw new Error("thrown inside setTimeout");
      } catch (e) {
        onEscaped("setTimeout: " + e.message + " — the boundary never saw this");
      }
    }, 50);
    return () => clearTimeout(t);
  }, [mode, onEscaped]);

  return (
    <div style={ok}>
      <div>rendering normally (mode: {mode || "idle"})</div>
      {/* ❌ NOT CAUGHT — React has finished rendering by the time this runs. */}
      <button onClick={() => {
        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>{" "}
      <button onClick={() => {
        // 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 };

export default function App() {
  const [mode, setMode] = useState("");
  const [escaped, setEscaped] = useState([]);
  const note = (s) => setEscaped((l) => [s, ...l].slice(0, 4));

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 560 }}>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 10 }}>
        <button onClick={() => setMode("render")}>throw in render</button>
        <button onClick={() => setMode("effect")}>throw in useEffect</button>
        <button onClick={() => setMode("timer")}>throw in setTimeout</button>
        <button onClick={() => { setMode(""); setEscaped([]); }}>reset all</button>
      </div>

      <Boundary key={mode}>
        <Subject mode={mode} onEscaped={note} />
      </Boundary>

      <div style={{ marginTop: 10, fontSize: 13 }}>
        <strong>escaped the boundary:</strong>
        <pre style={{ background: "#f6f6f8", padding: 8, borderRadius: 6, fontSize: 12, minHeight: 60 }}>
{escaped.length ? escaped.join("\n") : "(nothing yet)"}
        </pre>
      </div>

      <p style={{ 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>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 115 of 119 decoded in the React.js track. One more won't hurt.

Back to track