In Vue 3, in what order do `onErrorCaptured` hooks and `app.config.errorHandler` run for a descendant's error, and what does returning `false` change?
answer
- bubbles like a DOM event
- starts at the parent, not self
- every ancestor, bottom to top
- false means handled, stop here
basics
~20 sIn Vue 3 an error starts at the failing component's parent and climbs the parent chain, running every onErrorCaptured hook bottom to top, then app.config.errorHandler. A hook returning false marks it handled: later hooks and the app handler are skipped.
solid answer
~40 sWhen a component's code throws, Vue starts at that component's **parent** and walks up the parent chain. At each ancestor it calls the registered `onErrorCaptured(err, instance, info)` hooks in order; `instance` is always the component that threw. If any hook returns exactly `false`, propagation stops — no further ancestors and no `app.config.errorHandler`. If none does, the error reaches `errorHandler`, or Vue's default logging when there is none. Two consequences surprise people: a component's own `onErrorCaptured` never sees errors from its own setup or render — it only captures descendants — and an `async` hook returns a promise, never `false`, so it cannot stop propagation. If a hook itself throws, both that error and the original still travel on to `errorHandler`.
code
vue · 18 lines<!-- WidgetPanel.vue -->
<script setup lang="ts">
import { onErrorCaptured } from 'vue'
import RevenueChart from './RevenueChart.vue'
onErrorCaptured((err, instance, info) => {
console.warn('widget failed:', info, err)
// no return: DashboardPage, App and app.config.errorHandler still see it
})
onErrorCaptured((err) => {
if (err instanceof TypeError) return false // handled: nothing above runs
})
</script>
<template>
<RevenueChart />
</template>go deeper
Remember that onErrorCaptured catches errors from child components and that returning false stops the error going any further.
Walk through the chain step by step: parent first, every ancestor bottom to top, then errorHandler, and exactly what false skips.
Avoid the traps in real code: own errors not captured, async hooks that cannot return false, and false silently hiding errors from reporting.
Set a team rule for where errors are allowed to stop, so containment for users never costs visibility for the people on call.
## The walk `onErrorCaptured` registers a hook on a component that is called when an error **propagating from a descendant** is captured. It takes `(err, instance, info)` — the error, the public instance of the component that threw, and the source string — and may return `false`. When code in component `C` throws, Vue runs this walk: 1. Start at `C`'s **parent**, not at `C`. 2. Call each `onErrorCaptured` hook registered on that ancestor, in registration order. 3. If a hook returns `false`, stop: the error is considered handled. 4. Otherwise move to the next parent and repeat until the root. 5. If the walk finishes, call `app.config.errorHandler` if set, or fall back to Vue's default logging. The Vue docs compare this with DOM event bubbling: every hook on the chain sees the same error, from the bottom up, unless someone stops it. ## An example chain For `App > DashboardPage > WidgetPanel > RevenueChart`, where `RevenueChart` throws during render: | Step | Who is called | If it returns `false` | |---|---|---| | 1 | `WidgetPanel`'s hooks | nothing above runs | | 2 | `DashboardPage`'s hooks | `App` and `errorHandler` are skipped | | 3 | `App`'s hooks | `errorHandler` is skipped | | 4 | `app.config.errorHandler` | — | `RevenueChart`'s own `onErrorCaptured`, if it had one, is **not** in the list. ## What `false` means — and what it does not - It means "handled, stop here". It is the only return value that stops the walk; `undefined`, `true` or a promise all let it continue. - It does **not** recover anything by itself. The failed component has already been dealt with — a render that threw leaves an empty placeholder — and showing a fallback is up to the capturing component's state. - It hides the error from `app.config.errorHandler`, and with it from central reporting. Return `false` only where you have reported or genuinely handled the error. ## Traps - **Own errors are invisible.** A component cannot catch its own `setup()` or render errors with its own hook, because the walk starts at the parent. Put the hook one level up, in a wrapper component. - **Async hooks cannot stop propagation.** `onErrorCaptured(async (err) => { await report(err); return false })` returns a promise, which is not `false`, so the error keeps climbing. Keep the hook synchronous and fire the report without awaiting it. - **A hook that throws.** The new error is itself handled as an error from the capturing component, and the original error continues up the chain; both end up at `errorHandler` unless something stops them. - **Multiple hooks on one component** (from composables, say) all run, in registration order; any one returning `false` stops the rest. ## Options API and composables In the Options API the same hook is the `errorCaptured` option; `onErrorCaptured` is its Composition API form and must be called synchronously during `setup()`, like any lifecycle registration. A composable can register one on behalf of its component — useful for scoped reporting — but remember it applies to that component's **descendants** only. ## Choosing where to return `false` - **At a boundary that shows a fallback and has already reported the error** — returning `false` keeps outer boundaries from also switching to their fallbacks. - **Not in a hook that only adds context or logs** — let the error continue so the app handler still sees it. - **Never as a blanket default in a shared composable** — it would silently cut every descendant's errors off from central reporting for any component that uses it. - **Conditionally, by error type** — for example, stop expected validation errors but let everything else climb. A useful review question for any `return false`: who reports this error now? The answer interviewers want: parent first, bottom to top, every ancestor, then the app handler; `false` stops everything above; own errors are never captured by your own hook.
- Why does `onErrorCaptured(async (err) => { await report(err); return false })` not stop the error?An `async` function always returns a promise, and Vue checks the hook's return value with a strict comparison to `false`. A promise is not `false`, so propagation continues to the next ancestor and to `errorHandler`. Keep the hook synchronous: start the report without awaiting it and return `false` directly.
- A component registers `onErrorCaptured` and then throws in its own render. Which hook sees the error?Not its own. Vue starts the walk at the failing component's parent, so the component's hook is skipped and its parent's hooks are the first to run, then further ancestors and `app.config.errorHandler`. To contain a component's own failures, place the hook in a wrapper component one level up.
- What happens if an `onErrorCaptured` hook throws while handling an error?Vue handles the new error as an error from the capturing component, so it starts its own walk from that component's parent. The original error is not stopped — the throwing hook did not return `false` — so it keeps climbing too. Unless something higher returns `false`, `app.config.errorHandler` receives both.
It is an escalation ladder: a ticket goes to your manager, then theirs, up to the director (errorHandler). Anyone who marks it resolved (returns false) ends the escalation — and nobody receives tickets from their own desk, only from the people below them.
saying these in an interview costs you the question
- A component's onErrorCaptured catches errors thrown in its own render.
- Only the nearest onErrorCaptured hook runs; the rest are skipped automatically.
- Returning false from an async onErrorCaptured hook stops propagation.
- Returning false re-renders the failed component with fallback content.
- app.config.errorHandler runs before the onErrorCaptured hooks.