skip to content

Error Boundaries & Recovery

Containing a throw in one subtree: an ancestor that catches it, renders a fallback, and offers a way back — plus the throws it never sees. Asked because teams report errors but rarely recover.

on this pageshow

questions

6

In a component framework, what is an error boundary, and what happens when a component below it throws while rendering?

level: juniorimportance: must knowfreq 72%

answer

  1. containment, not prevention
  2. an ancestor with a fallback
  3. subtree replaced, rest of app survives
  4. covers render and commit throws only

basics

~20 s

An error boundary is an ancestor that catches a throw raised while the runtime renders or commits its subtree, renders a fallback in that subtree's place, and leaves the rest of the application mounted and usable.

solid answer

~50 s

Producing a screen means the runtime calls a lot of author code: render functions, template expressions, computed values, lifecycle callbacks. If one of them throws, the runtime is holding a tree it cannot finish, and with nothing above it to ask, most runtimes tear the tree down from the root — a blank page. An error boundary is an ancestor that declares "if anything below me throws while output is being produced or committed, give me the error and render this instead". On a catch it abandons the failed work, tears down its own children, and puts a **fallback** where they were; everything outside it keeps its instances, state and host nodes. Frameworks expose it as a wrapper component, as a captured-error lifecycle hook on an ancestor, or as one application-level handler — the same mechanism in three shapes. It contains the failure; it does not fix it.

go deeper

for a junior

Recall the one-liner: an ancestor that catches a throw from the subtree it wraps and shows a fallback instead, so the rest of the page survives. Know that it contains a failure and does not repair it.

for a middle

Explain the sequence — failed work abandoned, unwind to the nearest handler, children torn down, fallback rendered — and be precise that it covers throws raised while output is produced or committed, not throws from callbacks.

for a senior

Show the production instinct: every boundary needs a designed fallback, a report with component context, and an action that can change the outcome. Be able to say what the user loses for each placement you chose.

for a principal

Frame the tradeoff: boundaries buy containment but cost a designed fallback, copy and a reset path each, and a boundary over a correctness-critical region can present a confident page that is quietly wrong.

## What an error boundary is A component framework builds a screen by **calling your code**: a render function or a compiled template's expressions, a computed value, a lifecycle callback. One update therefore runs dozens or hundreds of small pieces of author code inside the runtime's own work loop. If any of them throws, the runtime is holding a half-built tree it cannot finish, and it has to decide what happens to the part of the screen that depended on it. An **error boundary** answers that question for one subtree. It is an **ancestor** which declares, in effect: *if anything below me throws while the runtime is producing or committing output, hand me the error and render this instead.* Frameworks expose the same idea in three shapes: - a **boundary component** you wrap a subtree in — it renders its children until it catches, then a fallback; - a **captured-error lifecycle hook** on an ordinary ancestor component, which the runtime calls with the error as it unwinds; - an **application-level handler** registered once with the runtime, which receives whatever no nearer handler consumed. They differ in ergonomics, not in mechanism: something above the failure is told about it and decides what the user sees. ## What catching actually does 1. The runtime **abandons the work in progress** for that part of the tree. The render that threw produced no usable output, so nothing partial is kept. 2. It **unwinds upward** from the throwing component to the nearest ancestor that declares a handler. 3. It **discards that boundary's children**: already-mounted instances below it are torn down, their cleanup runs, their host nodes are removed. 4. It **re-renders the boundary in its caught state**, which puts the fallback where the children used to be. Everything outside the boundary was not part of the failed work, so it is untouched. | | Inside the boundary | Outside the boundary | |---|---|---| | Component instances | torn down | keep running | | Local state | lost | preserved | | Host nodes | removed, fallback put in their place | unchanged | | Cleanup callbacks | run as part of the teardown | not involved | ## Why the alternative is so bad With no handler anywhere above it, a render-time throw propagates to the root, and most runtimes unmount the whole tree rather than leave a half-updated screen mounted. The user loses navigation, the shell and any unsaved input in a form three panels away — because one small component read a property of an object that happened to be missing. That asymmetry is the entire argument for boundaries: **the cost of a local failure should stay local.** ## What a boundary is not - It is **not a fix.** The code below still throws; the boundary only decides what occupies the space. Wrapping a known bug converts an obvious crash into a permanently broken panel. - It is **not a universal catch.** It covers throws raised while the runtime renders or commits. A throw inside an event handler, a timer callback or an unobserved rejected promise runs on a stack the runtime is not driving, and no boundary sees it; those need explicit handling and the page's global error and unhandled-rejection listeners. - It is **not** a way to keep the broken component on screen. There is no "skip the bad child, render its siblings" mode — the boundary's children are replaced as a unit. ## What a useful fallback contains The fallback is UI you design, not a built-in message. The good ones: - say, in the user's terms, which part stopped working, and leave surrounding chrome usable; - offer at least one action that can change the outcome — retry the failed work, reload the view, return to a known-good screen; - report the caught error with enough context that someone can find the component that produced it; - avoid claiming anything about data they no longer have. A panel that says "0 items" when it actually failed is worse than one that says it failed. A fallback reading only "Something went wrong", with no way forward, turns a crash the user knew how to escape by reloading into a dead end on a page that still looks alive. ## Where it sits matters A boundary protects only what it wraps, so placement decides how much the user loses. One boundary at the root keeps the process alive but still costs the whole screen. One around each region that can fail on its own — a widget with its own data source, an embed, a panel rendering user-supplied content — costs only that region, at the price of a fallback to design and maintain for each. Start from the question "is the rest of this page still worth using without this part?": where the answer is yes, that part deserves its own boundary.

  • If the boundary catches a throw from one deep child, why does the whole wrapped subtree disappear rather than just that child?
    Because the runtime cannot trust the partial result. The render that threw produced no output for that path, and the instances above it inside the boundary were mid-update. Re-rendering the boundary in its caught state replaces its children as a unit, which is the only state the runtime can describe consistently.
  • Does a boundary catch a throw raised in code that runs after the subtree is already on screen?
    Only if the runtime is producing output at the time — for instance re-running a component or a computed value for an update. A throw from a click handler, a timer or a settled promise happens on a stack the runtime is not driving, so it escapes to the platform's global error or unhandled-rejection handling instead.
  • Is one boundary at the root enough?
    It is the minimum: it stops a single bad component from blanking the page and gives you one place to report from. It is rarely sufficient, because the user still loses the entire screen for any failure, including one in a non-essential widget. Add boundaries around regions that can fail independently and whose absence still leaves a usable page.

It is the breaker panel for one circuit: the toaster shorts, that circuit goes dark, and the rest of the house keeps its lights. The breaker does not repair the toaster.

saying these in an interview costs you the question

  • Thinks a boundary catches every error in the app, including handlers and timers
  • Believes the failing component keeps rendering while only its broken part is skipped
  • Assumes that without a boundary only the broken component disappears from the page
  • Treats adding a boundary as fixing the bug rather than containing it
  • Ships a fallback with no action, leaving the user on a dead-end screen
open as a page

Which failures does a render-time error boundary not catch, and what catches those instead?

level: middleimportance: must knowfreq 62%

basics

~20 s

A boundary covers only throws raised while the runtime produces or commits output. A throw in an event handler, a timer callback or a rejected promise escapes to explicit handling and the page's global error and unhandled-rejection listeners.

open as a page

When error boundaries are nested and a component throws during render, which one catches the throw and what does it replace?

level: middleimportance: should knowfreq 55%

basics

~10 s

The nearest ancestor declaring a handler catches it, so an inner boundary wins over an outer one. It replaces the whole subtree it wraps with its fallback, not just the failing component.

open as a page

When a boundary reports a caught render error to monitoring, why is a stack trace alone rarely enough to locate the failure?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A trace names the functions on the stack, which for a render are mostly runtime internals plus one component type that may be mounted in dozens of places. The component path is what locates the failure.

open as a page

A caught error shows a fallback with a retry action. What must that reset actually do so the subtree does not fail again immediately?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Clearing the boundary's caught state is not enough: the reset must change what produced the throw — re-run the failed work with fresh inputs, or remount the subtree under a new identity so no stale state survives.

open as a page

How would you decide where error boundaries belong across a long-lived application, and which failures would you deliberately leave uncontained?

level: principalimportance: should knowfreq 40%

basics

~20 s

Put a boundary where a region can fail on its own and the page is still worth using without it; leave it out where a fallback would strand the user or make a wrong page look complete.

open as a page