Skip to the solution
HardFrontend7 min read

What can and can't an Error Boundary catch?

149 views

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 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.

  • react
  • error-handling
  • lifecycle

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read · 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactFour throw sites, one boundary — press each and see which are caught
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 115 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

Partial Prerendering (PPR) in Next.js 15 — static shell + dynamic holes

Continue