Which failures does a render-time error boundary not catch, and what catches those instead?
answer
- was the runtime on the stack?
- scheduled callbacks escape the loop
- handlers, timers, rejections, teardown
- global nets report but cannot contain
basics
~20 sA 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.
solid answer
~50 sA boundary is wired into the runtime's own work loop: the runtime calls your render code inside something equivalent to a `try`, so anything thrown there can be unwound to an ancestor handler. Code the runtime merely *scheduled* does not run inside that loop. An event handler is invoked by the host's event dispatch; a timer or animation callback by the host's scheduler; a rejected promise resolves in a microtask with no caller at all. Those escape to the platform: a global error listener, a global unhandled-rejection listener, or a `try`/`catch` you write yourself. Two more gaps matter in practice — a throw during teardown, when the runtime is already dismantling the tree a boundary would re-render, and a throw inside the boundary's own fallback. The important asymmetry: a global listener can report, toast or reload, but it cannot put a fallback in the failing subtree's place.
go deeper
Hold on to the boundary line: throws while the screen is being produced are caught; throws from clicks, timers and unobserved rejections are not, and need handling where they happen.
Justify the rule by the call stack — the runtime can only unwind code it called — and name the escapees plus the platform-level listeners that observe them.
Demonstrate the design response: turn anticipated operation failures into state, cancel work on teardown, keep cleanup unthrowable, and keep a global net purely for reporting.
Argue the coverage model as a whole: two disjoint mechanisms, one contains and one reports, plus a policy for what a global handler is allowed to do to a possibly inconsistent application.
## The rule behind the list A boundary is not a general exception hook; it is a feature of the runtime's **work loop**. When the runtime renders or commits, it is the caller of your code, so it can wrap that call and unwind to an ancestor handler if it throws. The single question that decides whether a boundary sees a throw is therefore: *was the runtime on the stack, producing or applying output, when the throw happened?* If you scheduled the code and the host called it back later, the runtime is not on the stack. It has no work in progress to abandon, no subtree to replace, and no reason to look for an ancestor handler. ## What escapes, and what catches it | Where the throw happens | Does a render boundary see it? | What can catch or observe it | |---|---|---| | A render function, template expression or computed value | yes | the nearest ancestor handler | | Applying changes to the host (the commit) | usually yes | the nearest ancestor handler | | An event handler invoked by host event dispatch | no | your own `try`/`catch`; the page's global error event | | A timer or frame callback | no | a `try`/`catch` in the callback; the global error event | | A rejected promise nobody observed | no | a rejection handler on the chain; the global unhandled-rejection event | | Code inside the boundary's own fallback | no — it is the boundary's output | the next handler above it | | A throw while a subtree is being torn down | often not recoverable | runtimes vary; treat it as report-only | | A throw in the server-rendered first pass | no mounted tree to swap | the request must degrade or fail | Two entries deserve unpacking. **Teardown.** When cleanup code throws, the runtime is already dismantling the very tree a boundary would re-render. There is nothing coherent to put a fallback into, and the failed cleanup has probably leaked whatever it was supposed to release — a subscription, a timer, an observer. Runtimes differ in whether such a throw reaches a handler at all, so the safe design is that cleanup never throws: guard it, and swallow-and-report rather than propagate. **Async work that outlives its component.** A request started by a component that has since been torn down can reject long after the subtree is gone. No boundary applies, because the component it belonged to no longer exists. This is why in-flight work wants cancellation on teardown rather than error handling after it. ## Why the global nets are not a substitute A global error listener and a global unhandled-rejection listener sit **outside** the component tree. They receive an error object and, at best, a stack; they have no idea which subtree was involved and no ability to render anything in its place. Their honest jobs are: - **report** the failure to monitoring, with the current view and build identity attached; - **notify** the user out-of-band — a toast, a banner in the shell; - **decide about the process** — offer a reload when the application's own state is likely corrupt; - **stop the default noise**, so the host does not just print to the console. What they cannot do is contain. That asymmetry is why "we have a global handler, we do not need boundaries" is wrong, and equally why "we have boundaries everywhere, we do not need global handlers" is wrong: the two cover disjoint failure sources. ## Designing for the gap 1. **Wrap intent, not everything.** In an event handler, wrap the risky call and turn the failure into state the component can render — an inline message, a disabled control, a retry affordance. That keeps the failure in the component's own vocabulary instead of relying on any global net. 2. **Never leave a rejection unobserved.** Attach handling where the work is started, so failure becomes data. An unhandled rejection is a bug report with no address on it. 3. **Cancel on teardown.** Tie in-flight work to the lifetime of the thing that started it, so results and rejections cannot arrive for a subtree that is gone. 4. **Make cleanup unthrowable.** Cleanup runs during a teardown that may itself be a recovery step; a throw there can turn one contained failure into an uncontained one. 5. **Keep a global net anyway.** It is the only thing that sees the classes above, and the only place a completely unexpected failure becomes visible at all. A last note on development builds: many runtimes deliberately surface caught errors loudly in development — rethrowing to the host, or showing an overlay — while production shows only your fallback. The same code can therefore look like a crash on a developer's machine and like a quiet fallback in production. Do not read that difference as two different bugs, and never rely on the development behaviour to tell you a failure is happening in production; only reporting does that.
- Why can the same throwing line be caught in one place and not another?Because catching depends on the caller, not the code. A helper called during render runs inside the runtime's work loop, so an ancestor handler can unwind it. The identical helper called from a click handler runs on the host's dispatch stack, where no boundary is involved and only your own `try`/`catch` or the global error event applies.
- Is turning a failed operation into component state better than letting a boundary catch it?For anything the user triggered, yes. Rendering an inline message keeps the surrounding UI and the user's input alive and lets you offer a precise next step, while a boundary would replace a whole subtree. Reserve boundaries for failures you did not anticipate; use explicit state for the ones you did.
- What should a global unhandled-rejection listener actually do in an application?Report with context — current view, build identity, a redacted description of the operation — and decide about user-facing noise. It should not try to repair state it knows nothing about. If such rejections indicate the application may be inconsistent, the honest offer is a reload rather than a silent continue.
saying these in an interview costs you the question
- Believes a boundary catches throws from event handlers and timers too
- Thinks an unobserved rejected promise reaches the nearest boundary
- Says a global error listener can render a fallback in the failing subtree
- Assumes a throw during cleanup is recoverable like a render throw
- Treats a loud development overlay as proof production is reporting the failure