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 / Scope | Will 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 code | ❌ | Hook 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
Pitfall | Fix |
---|---|
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.