Vue.js: Error Handling
Last updated: 2026-10-20
Error handling is an essential capability for any production-grade application—when something crashes you need to locate the cause fast, degrade gracefully, and tell the user what happened. Vue 3 offers several layers of error handling: errorHandler (global), onErrorCaptured (local), and the errorCaptured hook (component level).
Debugging and performance profiling matter just as much—Vue DevTools is a must-have tool that lets you inspect the component tree, state, events, and performance bottlenecks.
1. What You'll Learn
- 3 layers of error handling: errorHandler / onErrorCaptured / errorBoundary
- The 4 core features of Vue DevTools (component tree / state / events / performance)
- Performance profiling (performance API + DevTools)
- Sentry integration (production error monitoring)
- Source Map based error location
- 5 common debugging techniques
2. The White-Screen Crash Where "Nobody Can Find the Cause"
(1) Pain Point: One API Error Blanks the Entire Page
Alice's admin dashboard had a serious bug:
// ProductList.vue
const products = ref([])
onMounted(async () => {
const res = await fetch('/api/products')
products.value = await res.json() // ❌ Backend returns non-JSON → throws
})
User experience:
- The whole ProductList page goes blank
- The error only shows up in the console
- Users see an empty page, and refreshing doesn't help
Product manager Charlie:
"Alice, customers are complaining about a blank page. We need to: 1) stop the whole page from crashing, 2) show an error message, 3) report it to our monitoring system."
(2) The Vue Error Boundary Fix
User experience:
- The error is caught and does not propagate
- A friendly error page is shown
- The error is reported to Sentry automatically
- The user can click "Retry"
(3) Benefits
After adding error handling:
- White-screen crashes: 100% → 0
- Error location: console → Sentry dashboard
- User experience: crash → friendly message
- Observability: every error is traceable
3. 3 Layers of Error Handling
(1) Global Error Handling: app.config.errorHandler
// main.js
const { createApp } = Vue
import App from './App.vue'
const app = createApp(App)
// ✅ Global error handling
app.config.errorHandler = (err, instance, info) => {
console.error('Global error:', err)
console.log('Component:', instance)
console.log('Info:', info) // 'render' / 'watch' / 'lifecycle hook'
// Report to Sentry
Sentry.captureException(err)
// Notify the user
showErrorNotification('Something went wrong')
}
(2) Component-Level Errors: onErrorCaptured
(3) Comparing the Levels
| Level | API | Scope | Best for |
|---|---|---|---|
| Global | app.config.errorHandler |
All uncaught errors | Required in production |
| Component | onErrorCaptured |
Errors in child components | Local error boundaries |
| Async | try/catch / window.onunhandledrejection | Promise errors | Manual handling |
(4) The 5 Main Error Sources
// 1. Render errors (template syntax mistakes)
app.config.errorHandler = (err, instance, info) => {
if (info === 'render') console.error('Render error:', err)
}
// 2. Lifecycle hook errors
if (info === 'mounted') console.error('Mounted error:', err)
// 3. watch callback errors
if (info === 'watcher callback') console.error('Watch error:', err)
// 4. Custom directive errors
if (info === 'directive') console.error('Directive error:', err)
// 5. Event handler errors
if (info === 'v-on handler') console.error('Event error:', err)
▶ Example: Vue 3 Global Error Handling (⚠️ requires Vite project main.js)
// main.js
app.config.errorHandler = (err, instance, info) => {
console.error('[Global]', err, info)
// Report to Sentry / your own logging service
// Sentry.captureException(err)
}
Output:
No UI change.
Every uncaught error inside Vue (render, lifecycle, watcher, directive, or v-on handler) is printed once to the browser console prefixed with [Global], together with the error object and the info string.
Console:
[Global] TypeError: Cannot read properties of null (reading 'foo') render
4. Debugging with Vue DevTools
(1) The 4 Core Panels
| Panel | What it shows | Best for |
|---|---|---|
| Components | Component tree, state, props, emits | Understanding structure, debugging data |
| Timeline | Events, lifecycle, performance | Tracing event flow, finding bottlenecks |
| Pinia/Vuex | Store state | Debugging global state |
| Routes | Route history | Debugging navigation |
(2) Using the Components Panel
1. Open DevTools (F12 / Cmd+Opt+I)
2. Switch to the Vue tab
3. Click a component in the tree
4. The right panel shows:
- State (reactive data)
- Props
- Emits
- Slots
- Lifecycle
(3) 5 DevTools Techniques
// 1. Edit state live (dev mode)
// Edit directly in DevTools and watch the component react immediately
// 2. Time travel (Pinia)
// Switch to the Pinia panel → Base → replay each state change step by step
// 3. Performance marking
// import { markRaw } from 'vue'
// Mark large data objects with markRaw so DevTools does not traverse them deeply
// 4. Component highlight
// DevTools → the "eye" icon in the top right → hover to highlight components
// 5. Route navigation
// DevTools → Routing tab → inspect the path + params of every navigation
(4) Installing Vue DevTools
# Chrome extension
# https://chromewebstore.google.com/detail/vuejs-devtools/odjccnclnlddjlajjphfdmhnlhaglgki
# Firefox extension
# https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/
# Or the standalone app (recommended)
# https://devtools.vuejs.org/
5. Performance Profiling
(1) The Vue 3 Performance API
const { onMounted, onUnmounted } = Vue
onMounted(() => {
performance.mark('app-start')
// Your code
loadData()
performance.mark('app-end')
performance.measure('app-load', 'app-start', 'app-end')
const measure = performance.getEntriesByName('app-load')[0]
console.log(`App loaded in ${measure.duration}ms`)
})
(2) 5 Performance Optimization Techniques
// 1. v-once: render only once
<h1 v-once>{{ title }}</h1>
// 2. v-memo: cache a subtree
<div v-memo="[item.id, item.updatedAt]">
<!-- Re-renders only when id or updatedAt changes -->
</div>
// 3. shallowRef: no deep reactivity for large data
const bigList = shallowRef([...10000 items])
// 4. markRaw: keep third-party libraries out of reactivity
const map = markRaw(new Map())
// 5. Lazy loading: routes / components
const Heavy = defineAsyncComponent(() => import('./Heavy.vue'))
(3) The 5 Key Performance Metrics
| Metric | Target | How to measure |
|---|---|---|
| FCP (First Contentful Paint) | < 1.8s | Lighthouse |
| LCP (Largest Contentful Paint) | < 2.5s | Lighthouse |
| TTI (Time to Interactive) | < 3.8s | Lighthouse |
| TBT (Total Blocking Time) | < 200ms | Lighthouse |
| CLS (Cumulative Layout Shift) | < 0.1 | Lighthouse |
▶ Example: 5 Performance Optimizations (quick reference)
// 1. v-once (render only once)
<h1 v-once>{{ title }}</h1>
// 2. v-memo (cache)
<div v-memo="[item.id]">...</div>
// 3. shallowRef (shallow reactivity)
const list = shallowRef([...])
// 4. markRaw (no reactivity)
const map = markRaw(new Map())
// 5. Async component (lazy loading)
const Heavy = defineAsyncComponent(() => import('./Heavy.vue'))
Output:
These are snippets rather than a runnable page.
Once applied: the v-once heading renders a single time and never updates again;
the v-memo block re-renders only when item.id changes;
shallowRef stops Vue from deeply tracking the 10,000-item array, so mutations inside it are far cheaper;
markRaw keeps a Map instance outside the reactivity system;
and the async component emits a separate chunk that is fetched only when first rendered.
Effect on rendering:
v-once → 1 render total, later updates skipped
v-memo → re-render only when item.id changes
shallowRef → deep values are not tracked (assign .value to trigger)
markRaw → object never becomes reactive
async import → separate chunk loaded on demand
6. Sentry Integration
(1) Installation
npm install @sentry/vue @sentry/tracing
(2) main.js Configuration
const { createApp } = Vue
import * as Sentry from '@sentry/vue'
import { Integrations } from '@sentry/tracing'
import App from './App.vue'
const app = createApp(App)
// 1. Initialize Sentry (the app instance must exist first)
Sentry.init({
app,
dsn: 'https://your-dsn@sentry.io/123',
integrations: [
new Integrations.BrowserTracing()
],
tracesSampleRate: 1.0,
// Lower the sample rate in production
// tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0
// Vue-specific options
logErrors: true,
release: '1.0.0',
environment: process.env.NODE_ENV
})
// 2. Report global errors
app.config.errorHandler = (err, instance, info) => {
Sentry.captureException(err, {
extra: {
component: instance?.$options.name,
info
}
})
}
(3) Source Map Configuration
// vite.config.js
export default {
build: {
sourcemap: true // Generate source maps in production too
}
}
// Upload source maps to Sentry
// @sentry/cli releases -o your-org -p your-project files upload-sourcemaps ./dist
(4) 5 Sentry Features
| Feature | What it does |
|---|---|
| Error monitoring | Captures every error automatically, viewable in a dashboard |
| Performance tracing | Locates slow requests and slow DB queries |
| Session replay | Records what the user did to see the error in context |
| Release health | Compares the error rate of each release |
| Alerts | Slack / Email notifications |
7. Putting It Together: Error Boundary in Action
▶ Example: 3 Layers of Error Handling (ErrorBoundary demonstrable via CDN)
▶ Try it YourselfOutput:
The page renders a toggle button and two independent ErrorBoundary regions.
Initially both show their normal content. Clicking the button mounts the buggy child;
its render error is caught by the enclosing boundary, which replaces only that region with a red "⚠️ Error caught: ..." box and a retry button, while the other boundary keeps rendering normally.
Clicking the button again restores the content.
┌───────────────────────────────────────────────┐
│ [Trigger child error] │
│ │
│ Outside ErrorBoundary - works normally │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ ⚠️ Error caught: Cannot read properties │ │
│ │ of null (reading 'foo') [Retry] │ │
│ └───────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
The rest of the page keeps working; only the faulty
boundary shows the error UI.
(1) 📊 The 5 Main Error Sources (info field)
| info value | Meaning | Example |
|---|---|---|
'render' |
Render error | Template references an undefined variable |
'mounted' / 'updated' |
Lifecycle error | Accessing the DOM inside onMounted fails |
'watcher callback' |
Watch error | A watch callback throws |
'directive' |
Directive error | A custom directive throws in mounted |
'v-on handler' |
Event error | An @click handler throws |
(2) 📊 Quick Reference for 5 Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| White-screen crash | One component error kills everything | Wrap it in an ErrorBoundary |
| Swallowed error | Child component fails silently | Don't forget return false in onErrorCaptured |
| Missing Source Map | Production errors can't be located | vite build with sourcemap: true |
| Poor performance | A 1000-item list stutters | v-memo + shallowRef |
| Memory leak | Timers keep running after unmount | Clean up in onUnmounted |
(3) 📊 5 Debugging Scenarios
| Scenario | Tool |
|---|---|
| Unexpected component state | Vue DevTools → State |
| Event doesn't fire | Vue DevTools → Timeline |
| Wrong route navigation | DevTools → Routing |
| Production crash | Sentry dashboard |
| Slow performance | Lighthouse + DevTools Performance |
❓ FAQ
errorHandler only catches synchronous errors inside Vue. Async errors (Promise / setTimeout) need try/catch or window.addEventListener('unhandledrejection').Vue.config.devtools = false (to avoid exposing internal structure).<div v-for="item in items" :key="item.id" v-memo="[item.id, item.updatedAt]">, this div only re-renders when id or updatedAt changes.errorCaptured hook; Vue 3 uses the onErrorCaptured Composition API. Vue 3 also adds the global app.config.errorHandler.📖 Summary
- 3 layers of error handling: global (errorHandler) + component (onErrorCaptured) + async (try/catch)
- 5 main error sources: render / lifecycle / watch / directive / event
- The 4 Vue DevTools panels: Components / Timeline / Pinia / Routes
- 5 performance optimizations: v-once / v-memo / shallowRef / markRaw / lazy loading
- Sentry integration: automatic capture + performance tracing + Source Maps
- 5 performance metrics: FCP / LCP / TTI / TBT / CLS
- Production essentials: ErrorBoundary + Sentry + Source Maps
📝 Exercises
-
Basic (difficulty ⭐) Build an ErrorBoundary component:
- onErrorCaptured catches errors from child components
- Show a friendly error page
- Provide a "Retry" button
-
Intermediate (difficulty ⭐⭐) Build a complete error handling system:
- Global errorHandler reporting to the console
- Component-level ErrorBoundary
- try/catch for async errors
- Classification of the 5 error sources
-
Challenge (difficulty ⭐⭐⭐) Build a complete "production-grade" debugging and error monitoring setup:
- ErrorBoundary components in 3 different places
- Sentry integration (production)
- Source Map configuration
- The 5 Vue DevTools debugging techniques
- The 5 performance optimizations (v-memo / shallowRef / markRaw / lazy loading / v-once)
- Monitoring of the 5 performance metrics (Lighthouse integration)