Error Handling in Vue Applications

Intermediate
11 min

Error Handling in Vue Applications

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.

Where Errors Come From

| 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.

Component-Level: onErrorCaptured and Error Boundaries

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:

vue
<!-- 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).

Application-Level: app.config.errorHandler

Errors that no boundary stops reach the global handler. Use it to log once, report to a monitoring service and show a generic message:

javascript
// 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.

Errors Vue Does Not See

Add browser-level listeners for the rest, and hook Vue Router for navigation problems:

javascript
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.

Reporting to a Monitoring Service

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.

Common mistakes

  • Swallowing errors in onErrorCaptured by returning false without logging or reporting them.
  • Wrapping the entire app in a single boundary, which replaces the whole page for one widget's failure.
  • Throwing plain strings; always throw Error objects so stack traces and message exist.
Quick Quiz
Question 1 of 2

What does returning `false` from an `onErrorCaptured` hook do?

Key Takeaways

  • Vue catches errors from rendering, watchers, lifecycle hooks, template event handlers and async setup; other async code needs 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.
  • Handle expected failures explicitly with 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.

Error Handling in Vue Applications - Vue.js | CodeYourCraft | CodeYourCraft