An uncaught error in a render function or lifecycle hook can leave part of the page blank with no explanation, and an unhandled promise rejection disappears into the console. Vue provides hooks to catch errors at the component level and at the application level, and the browser provides the rest. After this lesson you will build an error boundary component, install a global handler that reports to a monitoring service, and cover the cases Vue's hooks do not see.
| Source | Caught by Vue? |
| --- | --- |
| Render functions, templates, watchers, lifecycle hooks | Yes |
| Event handlers declared in templates | Yes |
| async setup and async components inside <Suspense> | Yes |
| Promises created in your own code and not awaited by Vue | No: use window events |
| Navigation failures and lazy route chunk failures | No: use router.onError |
| Errors thrown before createApp runs | No |
Vue catches errors that occur inside code it invokes. Anything that happens outside a Vue call stack, such as a setTimeout callback or a fire-and-forget fetch, must be handled by you.
onErrorCaptured runs when an error propagates from any descendant component. It receives the error, the component instance and an info string describing the source (render, setup function, native event handler, ...). Returning false stops the error from propagating further up:
<!-- ErrorBoundary.vue -->
<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((err, instance, info) => {
error.value = err
console.error(`[${info}]`, err)
return false
})
function retry() {
error.value = null
}
</script>
<template>
<div v-if="error" role="alert" class="error-box">
<p>Something went wrong: {{ error.message }}</p>
<button @click="retry">Try again</button>
</div>
<slot v-else />
</template>Wrap independent regions of the page: <ErrorBoundary><OrdersWidget /></ErrorBoundary>. When the widget throws, only that region shows the message and the rest of the dashboard keeps working. Because v-if unmounts the slot content, the retry button re-creates the child from scratch. This is the same pattern used for <Suspense> errors in the earlier lesson.
onErrorCaptured does not catch errors thrown by the component itself, only by its children. It also does not catch errors that happen in async code after the first await, unless that promise is one Vue is awaiting (async setup).
Errors that no boundary stops reach the global handler. Use it to log once, report to a monitoring service and show a generic message:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { reportError } from './monitoring'
const app = createApp(App)
app.config.errorHandler = (err, instance, info) => {
reportError(err, { component: instance?.$options.name, info })
if (import.meta.env.DEV) console.error(err)
}
app.mount('#app')Without a handler, Vue logs the error to the console; the failing subtree stops rendering while the rest of the app keeps working. A companion app.config.warnHandler receives development-only runtime warnings.
Add browser-level listeners for the rest, and hook Vue Router for navigation problems:
window.addEventListener('error', (event) => reportError(event.error))
window.addEventListener('unhandledrejection', (event) => reportError(event.reason))
router.onError((err, to) => {
// typical case: a lazy route chunk failed to load after a new deployment
if (err.message.includes('Failed to fetch dynamically imported module')) {
window.location.assign(to.fullPath) // full reload picks up the new build
}
})Inside components, prefer explicit handling for expected failures: wrap API calls in try/catch, store an error ref and render a message. Reserve boundaries and the global handler for unexpected errors.
Services such as Sentry provide a Vue integration (@sentry/vue) that installs as a plugin, hooks errorHandler and the window listeners, and attaches component names to each report. Whatever service you use, include the app version and current route so errors can be traced to a release.
onErrorCaptured by returning false without logging or reporting them.Error objects so stack traces and message exist.What does returning `false` from an `onErrorCaptured` hook do?
window listeners.onErrorCaptured in a boundary component isolates failures to one region; return false to stop propagation.app.config.errorHandler is the last line of defense; use it to report and show a generic message.router.onError handles failed navigations and stale lazy chunks after deployments.try/catch and an error ref; reserve boundaries for the unexpected.Next lesson: Testing with Vitest and Vue Test Utils — write unit and component tests that mount components, simulate events and assert on output.