Error Tracking

Send every unhandled error to one place with its release and component, upload source maps, and drop the noise you cannot fix.

Syntaxapp.config.errorHandler = (err, instance, info) => { /* report */ }

Error tracking is one place that collects the errors your running application throws, with enough context to find the cause. Without it you hear about breakage from users, days later, when the stack trace is gone.

Catching errors

Vue routes errors thrown inside components to app.config.errorHandler, a function of (err, instance, info). instance is the component that threw, and info is a string naming the source: a render function, a watcher callback, a named lifecycle hook. Errors outside Vue's call stack never reach it, so a rejected promise that nothing awaits needs the window's unhandledrejection event as well. For a single subtree, onErrorCaptured(callback) receives the same three arguments from descendants; returning false stops the error propagating, which also stops app.config.errorHandler from seeing it.

Source maps

Built code is minified, so an unaided trace reads t is not a function. Set build.sourcemap to 'hidden' in vite.config.ts. Vite then writes .map files but omits the comment that points at them, so browsers never download them. Upload those files to the tracker under a release identifier, then remove them from what you publish.

Noise

Real traffic throws errors you cannot fix: browser extensions, cancelled requests, bots on old devices. Filter them by message before sending. An inbox nobody reads is the same as having no tracking at all.

Attach the release to every report. Otherwise you cannot tell a year-old error from one you shipped an hour ago.

Example

Example · vue
// src/plugins/error-reporter.ts
import type { App, ComponentPublicInstance } from 'vue'

declare const __APP_VERSION__: string

const IGNORED = [/ResizeObserver loop/, /chrome-extension:/, /Load failed/]

interface Report {
  message: string
  stack: string | null
  source: string
  component: string | null
  release: string
  path: string
}

function report(
  err: unknown,
  source: string,
  instance: ComponentPublicInstance | null = null,
): void {
  const error = err instanceof Error ? err : new Error(String(err))
  if (IGNORED.some((pattern) => pattern.test(error.message))) return
  const payload: Report = {
    message: error.message,
    stack: error.stack ?? null,
    source,
    component: instance?.$options.name ?? null,
    release: __APP_VERSION__,
    path: window.location.pathname,
  }
  navigator.sendBeacon('/api/errors', JSON.stringify(payload))
}

export default {
  install(app: App): void {
    app.config.errorHandler = (err, instance, info) => {
      report(err, info, instance)
    }
    window.addEventListener('unhandledrejection', (event) => {
      report(event.reason, 'unhandledrejection')
    })
  },
}

When to use it

  • A user reports a blank screen and you need the stack trace plus the route it happened on.
  • Turning `t is not a function` from a minified bundle back into a file and line.
  • Deciding whether an error spike belongs to the release you shipped an hour ago.
  • Silencing a browser-extension error that fires on every page view so real reports stay visible.

More examples

Hidden source maps and a release identifier

`build.sourcemap: 'hidden'` writes the maps without the `//# sourceMappingURL` comment, so only your upload step reads them. `define` replaces the identifier `__APP_VERSION__` with the text on the right at build time, which is why string values must be wrapped in `JSON.stringify` — the value is inlined as source code, not as data. Add `declare const __APP_VERSION__: string` to `env.d.ts` so `vue-tsc` accepts it. In CI, upload `dist/**/*.map` under the same release string, then delete the maps before publishing `dist`.

Example · ts
// vite.config.ts
import { execSync } from 'node:child_process'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

const release = process.env.RELEASE ?? execSync('git rev-parse --short HEAD').toString().trim()

export default defineConfig({
  plugins: [vue()],
  define: {
    __APP_VERSION__: JSON.stringify(release),
  },
  build: {
    sourcemap: 'hidden',
  },
})

A boundary around one risky subtree

`onErrorCaptured` catches errors thrown by anything rendered in the slot, so a failing widget degrades to a message instead of blanking the screen. Returning `false` marks the error handled, which means `app.config.errorHandler` will not run — report inside the hook or the error is lost. The unused `instance` parameter is named `_instance` because the project's type checking flags unused parameters.

Example · vue
<script setup lang="ts">
import { onErrorCaptured, ref } from 'vue'

const failed = ref(false)

onErrorCaptured((err, _instance, info) => {
  failed.value = true
  navigator.sendBeacon(
    '/api/errors',
    JSON.stringify({
      message: err instanceof Error ? err.message : String(err),
      source: info,
      boundary: 'checkout-summary',
    }),
  )
  return false
})
</script>

<template>
  <p v-if="failed" role="alert">This summary is unavailable. Your cart is saved.</p>
  <slot v-else />
</template>

<style scoped>
p {
  color: #b00020;
}
</style>

A hosted tracker instead of your own endpoint

A service replaces the hand-written reporter once you want grouping, release comparison and alerting. The Vue SDK takes the application instance as the `app` option of `Sentry.init` rather than being installed with `app.use`, and it wraps `app.config.errorHandler` itself, adding the component name and the names of its props. Passing `router` to `browserTracingIntegration` ties timings to route names. The `release` value must be the same string the source maps were uploaded under, or traces stay minified.

Example · ts
// src/main.ts
import { createApp } from 'vue'
import * as Sentry from '@sentry/vue'
import App from './App.vue'
import router from './router'

declare const __APP_VERSION__: string

const app = createApp(App)

Sentry.init({
  app,
  dsn: import.meta.env.VITE_SENTRY_DSN,
  release: __APP_VERSION__,
  integrations: [Sentry.browserTracingIntegration({ router })],
  tracesSampleRate: 0.1,
})

app.use(router)
app.mount('#app')

Discussion

  • Be the first to comment on this lesson.