Vue Tutorial · Lessons

Foundation

Basic

Medium

Advanced

Error Handling & Debugging

onErrorCaptured, app.config.errorHandler, error boundaries, Vue DevTools and common warnings.

Sooner or later a component throws: an API returns something unexpected, a value is null where the template expected an object, a handler has a bug. Vue gives you three layers for dealing with that: onErrorCaptured in a parent component, a global app.config.errorHandler, and good old try/catch. This lesson shows each one working, then covers Vue DevTools and the console warnings you'll see most.

Try it: an error boundary

The card below is wrapped in an error boundary: a component that catches errors from anything inside it and shows a fallback instead of a broken page. Break the render, throw in a click handler, or trigger an async error from a real API call that returns 404. Then press Try again.

Where errors can come from

Vue catches errors thrown in almost every place it runs your code, and sends them up the component tree:

Source Example Caught by Vue?
setup() Reading a property of undefined Yes
Render / template {{ task.assignee.name }} when assignee is null Yes
Lifecycle hooks onMounted(() => { … }) Yes
Watchers and computed A watch callback that throws Yes
Event handlers @click="save" where save throws Yes
Async event handlers @click="load" where load is async and rejects Yes: Vue catches the returned promise
setTimeout, fetch().then() you don't return A callback outside Vue's call No: it goes to window.onerror / unhandledrejection

The last row is the one that bites. If you start async work that Vue can't see, catch its errors yourself:

onMounted(() => {
  setTimeout(() => {
    try {
      riskyThing()
    } catch (e) {
      error.value = e.message
    }
  }, 1000)
})

onErrorCaptured: handle errors from children

onErrorCaptured(err, instance, info) runs in a parent whenever a descendant throws. Return false to say "handled" and stop the error travelling further up. This is the boundary from the demo, written with a render function so it can swap its whole content:

public/tutorial-assets/vue/error-handling.js

const ErrorBoundary = {
    emits: ['caught'],
    setup(props, { slots, emit }) {
        const error = Vue.ref(null);
        const attempt = Vue.ref(0);

        Vue.onErrorCaptured((err, instance, info) => {
            error.value = err;
            emit('caught', { message: err.message, info });
            return false; // handled: stop it reaching app.config.errorHandler
        });

        function retry() {
            error.value = null;
            attempt.value++; // new key → children re-created from scratch
        }

        return () => error.value
            ? Vue.h('div', { class: 'warning', style: 'margin:0' }, [
                Vue.h('p', { style: 'margin:0 0 .5rem' }, [Vue.h('strong', 'Something went wrong in this panel. '), error.value.message]),
                Vue.h('button', { onClick: retry }, 'Try again'),
            ])
            : Vue.h('div', { key: attempt.value }, slots.default ? slots.default() : []);
    },
};

Using it is just wrapping:

<ErrorBoundary>
  <TaskCard slug="no-such-project" />
</ErrorBoundary>
The Try again button changes the wrapper's key. A new key makes Vue throw away the broken children and create fresh ones, so their state starts clean. Without it, the child would re-render with the same bad data and fail again.
onErrorCaptured only sees errors from descendants, not from the component that registers it. That's why a boundary is a separate wrapper component.

app.config.errorHandler: the global net

Anything not handled by a boundary reaches the app-level handler. This is where you report errors to a monitoring service. Without one, Vue just logs to the console.

const app = createApp(App)

app.config.errorHandler = (err, instance, info) => {
  // instance: the component that threw (or null)
  // info: where it happened, e.g. "native event handler"
  reportToMonitoring(err, { component: instance?.$options.name, info })
}

app.mount('#app')

Try it: each button throws from a different place, and the handler below records it.

In development, info is a readable label like "setup function" or "watcher callback". Production builds (including the one these demos use) pass a short link instead, such as https://vuejs.org/error-reference/#runtime-5, to save bytes. The page it points to explains the code.

In a Vue + Laravel app, a common pattern is to POST the error to a small Laravel endpoint that writes it to the log, so front-end and back-end errors end up in the same place. Services such as Sentry ship a Vue integration that installs this handler for you.

Handling expected errors with try/catch

Boundaries and global handlers are for unexpected failures. When failure is a normal outcome, such as a 404 or a validation error, handle it where it happens and show a proper message:

fetch only rejects on network failure, not on HTTP errors, so check response.ok yourself. The Fetching Data lesson builds this into a reusable composable.

app.config.warnHandler

Vue also emits warnings: problems that don't crash, such as a missing required prop. app.config.warnHandler(msg, instance, trace) lets you route them somewhere, for example to fail tests on any warning.

Warnings exist only in development builds. Production builds, including the one this site loads, strip them out entirely, so warnHandler never fires in production. Use the development build (vue.global.js, or npm run dev with Vite) while building.

Vue DevTools

The Vue DevTools browser extension (or the Vite plugin, vite-plugin-vue-devtools) adds a Vue panel to the browser's developer tools:

Tab Shows
Components The live component tree, each one's props, state and computed values, editable in place
Timeline Events, emits, Pinia actions and router navigations, in order
Pinia Every store's state, with time-travel
Router Current route, params and the route table

DevTools only connects to development builds. Start there whenever a value "should" be different: you can see exactly what each component holds.

Common warnings and their fixes

These are the console messages you'll meet most while developing:

Warning Usual cause Fix
Missing required prop: "task" Parent forgot the prop, or misspelled it Pass it, or check :task vs task and camelCase vs kebab-case
Failed to resolve component: TaskCard Not imported or not registered Import it in <script setup>, or register it globally
Property "count" was accessed during render but is not defined Used in the template, not returned or declared Declare it in <script setup> (or return it from setup())
Extraneous non-props attributes (class) were passed Component has several root nodes Bind v-bind="$attrs" on one of them, or use one root
Avoid app logic that relies on enumerating keys on a component instance Code looping over this Loop over your own data instead
Cannot read properties of null (reading 'name') Template reads into data that hasn't loaded Guard with v-if="task" or optional chaining task?.assignee?.name
Maximum recursive updates exceeded A watcher or computed changes the value it depends on Break the loop: derive with computed instead of writing in a watcher
Duplicate keys / v-for without :key Missing or repeated :key Use a stable, unique id

Key takeaways

  • Vue catches errors from setup, render, lifecycle hooks, watchers and event handlers, including rejected promises returned by async handlers. Work it can't see, such as setTimeout, needs its own try/catch.
  • An error boundary is a wrapper component using onErrorCaptured. Return false to mark the error handled, show a fallback, and change a key to retry with fresh children.
  • app.config.errorHandler is the global net for anything unhandled: report errors to monitoring from there.
  • Handle expected failures such as 404s and validation errors with try/catch where they happen, and check response.ok with fetch.
  • Warnings and Vue DevTools are development-only. Develop with the dev build and keep the console clean.