Harnessing Vue’s onErrorCaptured Lifecycle Hook for Robust Error Handling

When building complex single-page applications, unhandled runtime errors can quickly snowball into broken user experiences and difficult debugging sessions. Vue 3 ships with a powerful—but often overlooked—tool to intercept those exceptions close to where they happen: the onErrorCaptured lifecycle hook.

In this post we’ll explore what onErrorCaptured does, when it fires, and practical patterns for logging, recovering, and testing error states in your Vue components.

What exactly is onErrorCaptured?

onErrorCaptured is a composition-API variant of the Options-API errorCaptured option.

It lets a component listen for uncaught errors propagated from its child component tree (including async errors thrown in setup, lifecycle hooks, watchers, template event handlers, etc.).

import { defineComponent, onErrorCaptured } from 'vue';

export default defineComponent({
  setup() {
    onErrorCaptured((err, instance, info) => {
      // 1. err      → the Error (or thrown value)
      // 2. instance → the component instance where it originated
      // 3. info     → string describing the execution context
      console.error('[UI error]', info, err);
      // return false to stop further propagation
    });
  },
});


If your callback returns false, Vue halts the bubbling process—preventing ancestor components (and the global app handler) from seeing the same error. Otherwise, the error continues upward until handled or logged by Vue’s global error handler.

Execution Timing & Scope

State / ScopeWill onErrorCaptured Fire?Notes
Child’s setup() / lifecycle hooks✔️Sync & async
Template event handlers (@click)✔️Even inside v-if branches
Watchers / computed getters✔️Both deep & immediate
Parent’s own codeHook only sees descendant errors


If you need to catch this component’s own errors, attach a global handler or wrap risky logic in local try / catch blocks.

A practical example—graceful fallback UI

<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue';
import RiskyWidget from './RiskyWidget.vue';

const hasError = ref(false);

onErrorCaptured((err) => {
  hasError.value = true;
  // forward to a logging service
  reportToSentry(err);
  // stop propagation so parent doesn’t also render its error state
  return false;
});
</script>

<template>
  <RiskyWidget v-if="!hasError" />
  <ErrorBanner v-else message="Something went wrong loading the widget." />
</template>

Why this works

  • The wrapper component doesn’t care where in RiskyWidget the exception arose—it just flips to a fallback UI.
  • By returning false, you ensure the error is “handled” here, avoiding duplicate toasts higher up.

Integrating with global error tracking


Most teams pair local capture with an app-level handler registered via app.config.errorHandler. Use onErrorCaptured to supplement it with contextual recovery:

import { createApp } from 'vue';
import * as Sentry from '@sentry/vue';

const app = createApp(App);

app.config.errorHandler = (err, vm, info) => {
  Sentry.captureException(err, { extra: { info } });
};

At component level you might still:

  • Provide user-friendly fallback UI
  • Roll back optimistic updates
  • Retry background requests

Testing error flows


Using Vitest (or Jest) you can assert that your component gracefully handles failures:

it('renders fallback on child error', async () => {
  vi.spyOn(console, 'error').mockImplementation(() => {});
  const wrapper = mount(Wrapper);

  // Force the child to throw
  wrapper.findComponent(RiskyWidget).vm.$emit('error', new Error('Boom'));

  await nextTick();
  expect(wrapper.text()).toContain('Something went wrong');
});


Remember to mock out console errors to keep test output tidy.

Common pitfalls & best practices

PitfallFix
Swallowing errors without logging—makes debugging production issues impossible.Always log or report the error before returning false.
Returning non-boolean values—Vue treats anything but false as “continue bubbling”Explicitly return false when you truly handled it.
Expecting to catch top-level await/Promise rejections not originating from a component context.Handle those in a global listener (window.addEventListener(‘unhandledrejection’, …)) or your network layer.

When to rely on onErrorCaptured vs. global handlers

Use onErrorCaptured when you want localized recovery or specialized logging (e.g., include feature flags, route params, or user actions leading up to the error).

Use the global handler for application-wide concerns—sending telemetry, showing a generic crash overlay, or refreshing the app after a fatal error.

Closing Thoughts

onErrorCaptured empowers Vue developers to create resilient components that can fail softly, showing friendly fallbacks instead of white screens and console spew. Coupled with a global error handler, it forms a robust strategy to surface, log, and recover from runtime problems—keeping your users happy and your debugging sessions short.

Next time you integrate a third-party widget or roll out a risky refactor, wrap it with a small component and tap into onErrorCaptured. Your future self (and your users) will thank you.

Leave a Comment

Your email address will not be published. Required fields are marked *