In Vue 3, which errors can `onErrorCaptured` and `app.config.errorHandler` see, and which never reach them, like a throw inside a `setTimeout` callback?
answer
- did Vue call the function
- a returned promise counts
- timers and raw listeners escape
- browser-level events as the backstop
basics
~20 sVue 3 routes only errors from code it calls itself: renders, setup, lifecycle hooks, watchers, v-on handlers, directive and transition hooks, plus promises those return. Timer callbacks, un-returned promises and addEventListener listeners never reach its handlers.
solid answer
~40 sVue can only catch what it calls. It wraps component renders, `setup()`, lifecycle hooks, watcher getters, callbacks and cleanups, `v-on` handlers on elements and components, custom directive hooks and transition hooks; when one of those returns a promise, Vue also catches its rejection. Everything else runs outside that wrapper: a `setTimeout` callback, a `.then()` chain started in `onMounted` but not returned, a listener added with `addEventListener`, module-level code, callbacks from a third-party library. Errors there never reach `onErrorCaptured` or `app.config.errorHandler`; they surface as uncaught exceptions or unhandled rejections. The fixes are to return or `await` the promise inside the hook or handler, catch and report inside the callback yourself, and keep the browser's `error` and `unhandledrejection` events as a global backstop.
code
vue · 24 lines<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
import { loadStats, reportError } from './stats'
// Caught by Vue: the async hook returns the rejected promise
onMounted(async () => {
await loadStats()
})
// Escapes Vue: the timer callback runs outside any Vue call
const timer = setInterval(() => {
try {
loadStats().catch(reportError) // floating promise handled here
} catch (err) {
reportError(err)
}
}, 30_000)
onBeforeUnmount(() => clearInterval(timer))
</script>
<template>
<p>Stats refresh every 30 seconds.</p>
</template>go deeper
Know that Vue's error hooks catch errors from component code such as renders, hooks and v-on handlers, not from every script on the page.
Explain the returned-promise rule and list what escapes: timers, floating promises, manual listeners and library callbacks.
Review code for floating promises in hooks, report escaped callbacks through the same pipeline, and add browser-level backstops.
Define one error-reporting pipeline that Vue's handler, callback wrappers and global backstops all feed, so coverage does not depend on each author.
## The rule: Vue can only catch what it calls Vue's error handling is a `try/catch` around the functions Vue itself invokes, plus a `.catch` on any promise they return. An error is routed into `onErrorCaptured` and `app.config.errorHandler` only if it is thrown **inside that call** or rejects a **promise returned from it**. There is no magic interception of the whole page. ## What Vue wraps The Vue API reference lists these sources: - **Component renders** — template expressions and render functions. - **Event handlers** — `v-on` listeners on elements (`info`: `'native event handler'`) and handlers for component events (`'component event handler'`). - **Lifecycle hooks** — `onMounted`, `onUpdated`, `onBeforeUnmount` and the rest. - **`setup()`** — the synchronous part of it. - **Watchers** — the getter, the callback and the cleanup function. - **Custom directive hooks** and **transition hooks**. Internally Vue also wraps a few more calls, such as function template refs and async component loaders, but the list above is the documented contract. ## Promises: returned versus floating For hooks, event handlers and watcher callbacks, Vue checks whether the function returned a promise and, if so, attaches a rejection handler that feeds the error into the same chain. | Code inside `onMounted` | Reaches Vue's handlers? | |---|---| | `async () => { await load() }` and `load` rejects | yes — the async function returns the rejected promise | | `() => load()` and `load` rejects | yes — the promise is returned | | `() => { load() }` and `load` rejects | **no** — nothing was returned | | `() => { load().then(render) }` and `render` throws | **no** — the chain floats | The difference between the second and third rows is a pair of braces: easy to miss in review. ## What escapes - `setTimeout`, `setInterval` and `requestAnimationFrame` callbacks: they run later, outside any Vue call. - Listeners attached with `addEventListener` (on `window`, a chart library's canvas, a socket), rather than with `v-on`. - Callbacks a third-party library invokes on its own schedule. - Module-level code and code in plain helper modules that run outside components. - Errors with no component context at all — the app handler is only consulted when a component instance is involved. These surface as uncaught exceptions or unhandled promise rejections in the browser, not as Vue errors. ## Closing the gaps 1. **Return or await** promises from hooks and handlers so Vue's wrapper sees the rejection. 2. **Catch at the edge** of escaped callbacks: wrap the timer or listener body in `try/catch` and send the error to the same reporter your `errorHandler` uses. 3. **Register a backstop** on the browser's `error` and `unhandledrejection` events for anything still missed; it has no component context, but it has the error. 4. **Move work inside Vue-called code** where possible — a `watch` on state instead of a manual listener, for instance — so errors join the component chain and the nearest `onErrorCaptured` can show a fallback. ## Reading `info` to tell sources apart In development the `info` argument names the source precisely, which is the fastest way to see whether an error came through a path you expected: | `info` in development | Where it was thrown | |---|---| | `'render function'` | a template expression or render function | | `'setup function'` | the synchronous body of `setup()` | | `'watcher getter'` / `'watcher callback'` / `'watcher cleanup function'` | the three parts of a watcher | | `'native event handler'` | a `v-on` listener on an element | | `'component event handler'` | a listener for an emitted component event | | `'directive hook'` / `'transition hook'` | custom directive or `<Transition>` hooks | In production the same argument is a short code; the Vue docs' Production Error Code Reference translates it. The interview test is the table above: candidates who can say why `() => { load() }` escapes and `() => load()` does not have understood the mechanism rather than memorised the list.
- Why does `onMounted(() => { fetchStats() })` lose a rejection that `onMounted(() => fetchStats())` reports?Vue catches a rejection only on the promise the hook returns. The arrow function with braces returns `undefined`, so the promise from `fetchStats()` floats and its rejection becomes an unhandled rejection. The concise arrow returns the promise, and Vue attaches its handler to it, routing the error to `onErrorCaptured` and `app.config.errorHandler`.
- How do you still report errors from timers and manual listeners in a Vue app?Wrap their bodies in `try/catch` and attach `.catch` to their promises, sending errors to the same reporter your `app.config.errorHandler` uses. As a global backstop, listen for the browser's `error` and `unhandledrejection` events. Where possible, replace the manual listener with Vue-called code such as a watcher, so errors join the component chain.
saying these in an interview costs you the question
- app.config.errorHandler catches every error on the page, including timers.
- Errors in addEventListener callbacks reach onErrorCaptured like v-on handlers.
- A rejected promise started in a hook is always caught, returned or not.
- Async lifecycle hooks are outside Vue's error handling.
- Watcher callbacks are not covered by onErrorCaptured.