Chapter 103: Vue ‘errorCaptured’ Lifecycle Hook
ErrorCaptured lifecycle hook
This hook is not like mounted or onBeforeUnmount that you use every day. It is a special emergency hook — the only built-in mechanism Vue gives you to catch JavaScript errors that occur in descendant components (children, grandchildren, etc.) during the rendering phase, lifecycle hooks, and event handlers.
In plain words:
When any child component (or deeper descendant) throws an uncaught error during render / lifecycle / event handling, Vue will automatically call errorCaptured on every ancestor component that has defined it — starting from the component closest to the error and walking up the tree.
If no one catches the error → it bubbles up to the global app.config.errorHandler (if set) → otherwise → Vue logs it to console and the app crashes / white screen of death.
1. When does errorCaptured get called? (exact situations)
Vue calls errorCaptured in these four specific cases:
- Error during render of a descendant component (render() function throws, template compilation error that surfaces during render)
- Error in a lifecycle hook of a descendant (except errorCaptured itself and unmounted)
- Error in a watcher (watch, watchEffect) of a descendant
- Error in an event handler (@click, custom emitted event handler) of a descendant
Important exclusions:
- Errors in your own component’s render / hooks / watchers → not caught by your own errorCaptured
- Errors in asynchronous code (inside setTimeout, Promise.then, async functions) → not caught
- Errors in custom directives lifecycle → not caught
- Errors in global error handler itself → not caught
2. Signature & Arguments (what you receive)
In Options API:
|
0 1 2 3 4 5 6 7 8 9 |
errorCaptured(err, instance, info) { // return true → stop propagation up the chain // return false → let error continue propagating } |
In Composition API (<script setup>):
|
0 1 2 3 4 5 6 7 8 9 10 11 |
import { onErrorCaptured } from 'vue' onErrorCaptured((err, instance, info) => { // same arguments // return true → stop propagation }) |
Arguments explained:
| Argument | Type | What it contains |
|---|---|---|
| err | Error | The actual JavaScript error object (err.message, err.stack) |
| instance | ComponentInternalInstance | The component instance that threw the error (the child/grandchild) |
| info | string | String describing where the error occurred (very useful for debugging) |
Common info values you will see:
- “render function”
- “setup function”
- “mounted hook”
- “updated hook”
- “watcher callback”
- “event handler”
- “vnode hook” (directives, transitions…)
- “component event handler” (emitted custom event)
3. Real, Practical Example – Error Boundary Component
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
<!-- ErrorBoundary.vue – reusable wrapper that catches child errors --> <template> <div> <!-- Show fallback UI when error occurs --> <div v-if="hasError" class="error-boundary"> <h2>Something went wrong.</h2> <p>{{ errorMessage }}</p> <pre v-if="errorInfo">{{ errorInfo }}</pre> <button @click="resetError">Try again</button> </div> <!-- Render children only when no error --> <slot v-else /> </div> </template> <script setup lang="ts"> import { ref, onErrorCaptured } from 'vue' const hasError = ref(false) const errorMessage = ref('') const errorInfo = ref('') // This is the key – catch errors from children onErrorCaptured((err, instance, info) => { hasError.value = true errorMessage.value = err.message || 'Unknown error' errorInfo.value = info console.error('Error captured in boundary:', { error: err, component: instance?.type?.name || 'Anonymous', location: info }) // Return true → stop propagating up the tree // (prevents the error from reaching global handler or crashing app) return true }) function resetError() { hasError.value = false errorMessage.value = '' errorInfo.value = '' } </script> <style scoped> .error-boundary { padding: 2rem; background: #fee2e2; border: 2px solid #dc2626; border-radius: 12px; color: #991b1b; text-align: center; } .error-boundary pre { background: #fff5f5; padding: 1rem; border-radius: 8px; overflow-x: auto; margin: 1rem 0; text-align: left; } </style> |
Parent usage – wrapping dangerous component
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<template> <div> <h1>Main App</h1> <!-- This child will throw an error on mount --> <ErrorBoundary> <BuggyComponent /> </ErrorBoundary> <p>Rest of the app keeps working</p> </div> </template> <script setup lang="ts"> import ErrorBoundary from '@/components/ErrorBoundary.vue' import BuggyComponent from '@/components/BuggyComponent.vue' </script> |
BuggyComponent.vue (deliberately broken)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<template> <div> <!-- This will throw during render --> {{ null.toUpperCase() }} </div> </template> <script setup> console.log('Buggy component mounted – but render will crash') </script> |
Result:
- BuggyComponent throws during render
- ErrorBoundary catches it via onErrorCaptured
- Shows nice fallback UI
- Rest of the app does not crash — error is contained
4. Important Rules & Gotchas (2026 Must-Know)
| Rule / Gotcha | Correct Behavior / Best Practice |
|---|---|
| Catches errors in descendants only | Not your own component’s render/hooks — only children & deeper |
| Propagation | Return true → stop propagating upward Return false → let it continue up the tree |
| Multiple boundaries | Vue calls errorCaptured on every ancestor that has it — from closest to root |
| Does not catch async errors | setTimeout, Promise.then, async functions — use try/catch inside them |
| Does not catch errors in event handlers of parent | Only descendant event handlers |
| Does not prevent app crash if unhandled | If no one returns true → error bubbles to app.config.errorHandler → then console |
| Global fallback | Set app.config.errorHandler = (err, instance, info) => { … } for uncaught errors |
| Still used in 2026? | Yes — in UI libraries, error boundaries, dashboard wrappers, mission-critical apps |
5. Modern Pattern – Reusable ErrorBoundary Component (2026 standard)
Almost every serious Vue 3 app has something like this:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<!-- ErrorBoundary.vue --> <template> <slot v-if="!hasError" /> <div v-else class="error-fallback"> <h2>Oops! Something broke</h2> <p>{{ error?.message || 'Unknown error' }}</p> <pre>{{ errorInfo }}</pre> <button @click="reset">Try again</button> </div> </template> <script setup lang="ts"> import { ref, onErrorCaptured } from 'vue' const hasError = ref(false) const error = ref<Error | null>(null) const errorInfo = ref('') onErrorCaptured((err, instance, info) => { hasError.value = true error.value = err errorInfo.value = info // Optional: send to error tracking (Sentry, LogRocket…) console.error('Captured error:', { err, info, instance }) // Stop propagation – error is handled return true }) function reset() { hasError.value = false error.value = null errorInfo.value = '' } </script> |
→ Wrap any potentially unstable subtree:
|
0 1 2 3 4 5 6 7 8 9 10 |
<ErrorBoundary> <HeavyChart :data="data" /> <LiveFeed /> <UserGeneratedContent /> </ErrorBoundary> |
Quick Summary Table – beforeUnmount vs unmounted vs errorCaptured
| Hook | Purpose | DOM still alive? | Cleanup here? | Catches child errors? |
|---|---|---|---|---|
| onBeforeUnmount | Last chance to clean up before removal | Yes | Yes | No |
| onUnmounted | Component is gone – final logging/telemetry | No | No | No |
| onErrorCaptured | Catch render/lifecycle/event errors in children | Yes | No | Yes |
Pro Tips from Real Projects (Hyderabad 2026)
- Always use onBeforeUnmount for cleanup — onUnmounted is too late
- Wrap risky parts of your app in <ErrorBoundary> components — prevents full app crash
- Combine onErrorCaptured with Sentry / LogRocket / Bugsnag — send errors with component name & info
- In SSR / Nuxt → onErrorCaptured runs only client-side
- Test error boundaries — throw errors in child components → make sure fallback UI appears & app doesn’t crash
- Return true in onErrorCaptured → stop propagation — prevents error from reaching global handler
Your mini homework:
- Create ErrorBoundary.vue as shown
- Wrap a buggy component that throws in render (null.toUpperCase())
- Toggle it with v-if → see error is caught, fallback shown, rest of app alive
- Remove return true → see error propagates (console error appears)
Any part confusing? Want full examples for:
- onErrorCaptured + Sentry integration?
- Error boundary with reset + retry logic?
- errorCaptured vs global app.config.errorHandler?
- Error handling in <Suspense> / <Transition> / <KeepAlive>?
Just tell me — we’ll build the next robust, crash-proof component together 🚀
