skip to content

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%

answer

  1. unwind upward, first match wins
  2. inner boundary beats outer
  3. the whole wrapped subtree is replaced
  4. the fallback has no boundary of its own

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.

solid answer

~50 s

The runtime unwinds from the throwing component toward the root and stops at the **first** ancestor that declares a handler; nearer wins, so a widget-level boundary catches before the page-level one above it. What gets replaced is that boundary's own children as a unit — the failing node's siblings and its intact ancestors inside the boundary go too, because none of that render can be trusted. Only one handler consumes a given throw, unless it rethrows or reports upward explicitly. Two consequences follow. Placement is the blast-radius dial: a boundary at the root means any failure costs the whole screen, one per independently failing region means the user loses only that region. And the fallback itself is unprotected by its own boundary — if it throws, the throw continues up to the next handler, and if there is none the tree unmounts.

go deeper

for a junior

Remember two facts: the closest boundary above the throw is the one that reacts, and what it shows replaces everything it wraps rather than just the broken component.

for a middle

Walk the unwind out loud — throwing node, upward search, first declared handler, that boundary re-renders in a caught state — and name what is lost inside versus preserved outside.

for a senior

Show placement judgment: which regions can fail without taking the page down, why the shell is a poor candidate, and why a fallback must be dull enough that it cannot throw.

for a principal

Talk about coverage as a policy rather than per-component instinct: which regions must be independently survivable, who owns each fallback, and how you avoid a mosaic of undesigned error states.

## How a throw finds its boundary While the runtime produces output it is walking a tree, calling author code at each node. A throw interrupts that walk. The runtime then **unwinds**: from the node that threw it moves up through ancestors, looking for the first one that declares it handles errors — a wrapper boundary component, an ancestor with a captured-error lifecycle hook, or, failing all of those, the application-level handler registered with the runtime. The first match consumes the error; ancestors above it are never told, unless the handler deliberately rethrows or forwards a report. **Nearer always wins.** This is the same shape as exception handling in a call stack, with the component tree standing in for the stack: an inner `try` catches before an outer one. So a boundary around a single widget catches before the page-level boundary that contains it, and the page-level boundary only sees failures from parts of the page that no inner boundary wraps. Frameworks differ in *where* the throw originates, and that changes which node the unwind starts from. A runtime that re-runs whole component functions raises the throw inside the component call itself, so the unwind starts at that component. A runtime with fine-grained tracking raises it inside one tracked computation, and the unwind starts from whichever component owns that computation. A compile-time runtime throws inside the generated update code for one node. In all three the search is the same: walk up the component ownership chain to the nearest declared handler. ## What "replaced" means A common misreading is that the boundary swaps out the failing component. It does not: it re-renders **itself** in a caught state, and its children are whatever that caught state renders — the fallback. So the unit that disappears is everything the boundary wraps: - the component that threw; - its siblings, which may have rendered perfectly well; - intermediate ancestors between it and the boundary, along with their local state; - host nodes for all of the above, plus any cleanup those instances had registered, which runs during the teardown. That is why granularity is a design decision rather than a detail. The boundary cannot scope the damage more finely than the subtree it encloses. | Boundary placement | What the user loses on one widget failure | Cost to you | |---|---|---| | One at the root | the entire screen, including navigation and shell | one fallback, minimal effort | | One per view or major region | that region; the shell and other regions stay | a handful of fallbacks | | One per independently failing widget | only that widget, in place, page still usable | a fallback, copy and reset per widget | | One around every component | almost nothing visually, but the page becomes a mosaic of error states nobody designed | unmaintainable | ## When the fallback itself throws A boundary does not protect its own fallback. The fallback renders as the boundary's output, at the boundary's own position in the tree, so a throw inside it unwinds **past** that boundary to the next handler above. With no handler above, the tree unmounts and the user gets the blank screen the boundary existed to prevent — which is why fallbacks should be deliberately dull: no data access that can fail, no dependence on the state that just broke, no rich formatting of a value that may be missing. A related trap: if the fallback re-renders the same children in an attempt to recover, the throw happens again immediately, the boundary catches again, and you get a render loop the user experiences as a flicker. Recovery has to change something first. ## Practical consequences 1. **Put a boundary where a failure is survivable.** If the rest of the page is still useful without this part, that part deserves its own boundary. If it is not — the shell, the navigation, the thing the page is *for* — a boundary there only means the user faces a fallback where the application used to be. 2. **Keep one at the top anyway.** Even with good inner coverage, an outermost handler is the difference between a controlled screen and a blank one, and it is the natural place to report anything the inner boundaries did not. 3. **Do not stack boundaries hoping for depth.** Only the innermost handler runs. Extra layers between it and the root cost you nothing but buy you nothing either; what they do buy is a net for the inner fallback's own failures. 4. **Remember that outside is genuinely untouched.** Sibling regions keep their state, their scroll position and their in-flight work. That is the property you are buying, and it is also why a user can keep working after a widget dies.

  • Can two boundaries on the same path both react to one throw?
    Not by default — the nearest handler consumes it. If you want an outer boundary or an application-level handler to know as well, the inner one has to forward it explicitly: report the error upward, or rethrow so the unwind continues. Many teams report from the innermost boundary and let placement stay single-owner.
  • Why do the failing component's healthy siblings disappear too?
    Because the boundary re-renders itself, and its children are now the fallback rather than the original subtree. The runtime is not editing the failed tree; it is replacing the boundary's output. Any state those siblings held is lost with them, which is a reason to keep independently useful widgets in their own boundaries.
  • What makes a fallback risky to write richly?
    It renders outside the protection of the boundary that shows it, so any throw inside it escapes to the next handler above — or blanks the page if there is none. Keep fallbacks to static structure and safe values: no reads of the data that just failed, no optional-chain-free property access on it.

saying these in an interview costs you the question

  • Says the outermost boundary catches first because unwinding starts at the root
  • Thinks only the throwing component is swapped out, siblings untouched
  • Believes a boundary also catches throws raised inside its own fallback
  • Expects every boundary on the path to run its fallback for one throw
  • Wraps the application shell in a boundary and calls the page contained