Error Boundaries in Vue & Nuxt
Errors in Vue applications that bring down the entire component tree are avoidable. onErrorCaptured, the global errorHandler, error.vue and NuxtErrorBoundary form a layered system that isolates errors, shows meaningful fallbacks and keeps the rest of the application alive.
Table of Contents
- 1. Why error boundaries are indispensable in Vue
- 2. onErrorCaptured: the local error boundary
- 3. Fallback UI: making errors visible without a crash
- 4. Global errorHandler: the last line of defense
- 5. error.vue in Nuxt 3: error pages at route level
- 6. NuxtErrorBoundary: granular error isolation
- 7. Asynchronous errors and useAsyncData
- 8. Error monitoring and Sentry integration
- 9. Comparing the error handling layers
- 10. Summary
- 11. FAQ
1. Why error boundaries are indispensable in Vue
A Vue application without well thought out error boundaries behaves like a house of cards the moment an unhandled runtime error occurs: a single component that throws an exception while rendering can unmount the entire component tree and leave the user with a blank page or a generic browser console message. That is unacceptable in a production application, especially when the error occurred in a sidebar or a widget that has nothing to do with the main content.
Vue 3 recognized this problem and offers onErrorCaptured, a lifecycle hook that can catch errors from its own component and all child components before they damage the rest of the application. Combined with the global app.config.errorHandler, the Nuxt-specific error.vue and the NuxtErrorBoundary component, a layered safety net emerges that isolates errors, logs them and gives the user contextual feedback. Anyone who consistently applies error boundaries in Vue reduces support requests and measurably improves the user experience during unavoidable error scenarios.
2. onErrorCaptured: the local error boundary
onErrorCaptured is the core of every local error boundary in Vue 3. The hook is registered in the parent component and receives three parameters: the error that occurred, the component instance in which it occurred, and a string with the error origin (e.g. "render", "setup function" or "watcher"). If the hook returns false, the error is not propagated further up, the parent components and the global handler never see it. If nothing or undefined is returned, the error propagates upward until it is either caught by another parent component or by the global handler.
The typical pattern for an error boundary component in Vue 3 uses onErrorCaptured together with a reactive hasError state to switch between normal slot content and a fallback UI. Important: the hook only catches errors from synchronous rendering, lifecycle hooks and watchers. Errors in asynchronous event handlers that are thrown outside the Vue lifecycle context must be handled separately. This limitation is not a bug but a deliberate design decision that keeps the behavior of error boundaries in Vue predictable.
<!-- ErrorBoundary.vue: reusable error boundary component -->
<template>
<slot v-if="!hasError" />
<slot v-else name="fallback" :error="capturedError" :reset="resetError">
<div class="error-boundary-fallback">
<p>An error occurred.</p>
<button @click="resetError">Reload</button>
</div>
</slot>
</template>
<script setup>
import { ref, onErrorCaptured } from 'vue'
const hasError = ref(false)
const capturedError = ref(null)
// Capture errors from all child components
onErrorCaptured((err, instance, info) => {
hasError.value = true
capturedError.value = err
console.error('[ErrorBoundary] caught error:', err.message, 'in', info)
// Return false to stop error propagation
return false
})
function resetError() {
hasError.value = false
capturedError.value = null
}
<\/script>
3. Fallback UI: making errors visible without a crash
A good fallback UI for a Vue error boundary clearly communicates to the user that something went wrong, without overwhelming them with technical detail. The minimal goal: the user understands what did not work, can either take an action (reload, navigate back), or knows that the rest of the application is still usable. A fallback UI for a product list should not lock the entire shop but only replace the affected widget.
The named-slot pattern of the ErrorBoundary component from the previous section makes it possible to define a specific fallback UI per usage site. The wrapper component can reveal as much or as little about the error as the context allows: in the development environment you show the stack trace, in production a user-friendly message with an error ID. The error ID retrieved from the monitoring system immediately gives the support team the context they need. This approach with error boundaries in Vue makes error messages both usable and debuggable.
4. Global errorHandler: the last line of defense
Not every component is wrapped by a local error boundary. For all errors that find no local handler, there is app.config.errorHandler. This global handler is registered in main.js or in a Nuxt plugin and receives the same parameters as onErrorCaptured: error, component and error origin. It is the last opportunity to log an error before Vue passes it on to the browser console.
A well-structured global errorHandler distinguishes between critical errors that render a page unusable and tolerable errors that only affect a widget. It sends structured error information to an external monitoring system such as Sentry, LogRocket or a custom logging endpoint and shows a page-wide error display for critical errors. In Nuxt 3 this handler is partly replaced by Nuxt's own mechanisms, but as a fallback for internal Vue errors it remains irreplaceable. Anyone who does not configure the global errorHandler loses valuable error data from production.
// plugins/error-handler.client.js: Nuxt 3 plugin for global error handling
export default defineNuxtPlugin((nuxtApp) => {
// Global Vue error handler, catches all unhandled component errors
nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
const componentName = instance?.$options?.name ?? 'unknown'
// Log structured error data
console.error('[GlobalErrorHandler]', {
message: error.message,
component: componentName,
lifecycleHook: info,
stack: error.stack,
})
// Send to monitoring (e.g. Sentry)
if (typeof $sentry !== 'undefined') {
$sentry.captureException(error, {
extra: { component: componentName, vueInfo: info },
})
}
}
// Catch unhandled promise rejections outside Vue lifecycle
if (import.meta.client) {
window.addEventListener('unhandledrejection', (event) => {
console.error('[UnhandledPromise]', event.reason)
})
}
})
5. error.vue in Nuxt 3: error pages at route level
Nuxt 3 ships with error.vue in the project root, its own mechanism for error pages at route level. This component is displayed automatically when Nuxt itself detects a fatal error, for example a 404, a 500 from the server, or an error during server-side rendering. The component receives the error prop with statusCode, statusMessage and message. With useError() you have access to the current error state from any component, and clearError() resets the error state and optionally navigates to a different route.
A common mistake when using error.vue: treating all status codes the same and always showing the same message. It is better to offer a search or related content for 404, a support contact option for 500, and a retry button for network errors. Nuxt 3 makes this clean via error.statusCode in the template. error.vue is deliberately not a layout component, it is rendered outside the normal Nuxt layout. Anyone who wants to include the main layout anyway must explicitly import and use it inside error.vue.
6. NuxtErrorBoundary: granular error isolation
NuxtErrorBoundary is Nuxt's own implementation of an error boundary that goes beyond the global error.vue. It makes it possible to protect individual sections of a page against errors without the entire page jumping to the error page. The @error event handler receives the error and enables local reactions. The #error slot shows the fallback UI within the boundary. With clearError from the slot scope, the error state can be reset without reloading the page.
In practice, NuxtErrorBoundary is particularly suited for widgets that load data from external APIs. If the API request fails, the boundary shows an error message only for that area, the rest of the page, including navigation and other widgets, remains functional. This granularity is the decisive difference from a page-wide error page. Anyone who consistently applies NuxtErrorBoundary builds an application that stays usable for the user even during partial outages of external services.
<!-- pages/dashboard.vue: granular error isolation with NuxtErrorBoundary -->
<template>
<div class="dashboard-grid">
<NuxtErrorBoundary @error="onWidgetError">
<WeatherWidget />
<template #error="{ error, clearError }">
<div class="widget-error">
<p>Weather data unavailable.</p>
<button @click="clearError()">Try again</button>
</div>
</template>
</NuxtErrorBoundary>
<NuxtErrorBoundary @error="onWidgetError">
<StockTickerWidget />
<template #error="{ error, clearError }">
<div class="widget-error">
<p>Quote data unavailable.</p>
<button @click="clearError()">Try again</button>
</div>
</template>
</NuxtErrorBoundary>
</div>
</template>
<script setup>
// Centralized widget error tracking
function onWidgetError(error) {
console.warn('[Dashboard] widget error caught:', error.message)
// Report to monitoring without crashing the page
}
<\/script>
7. Asynchronous errors and useAsyncData
Asynchronous data errors are the most common source of poor user experiences in Nuxt 3. useAsyncData and useFetch do not return a promise on a failed fetch that stays unhandled, instead the error information lands in the error ref that the composable returns. This enables declarative error handling directly in the template, without try-catch blocks. The status ref with the values "idle", "pending", "success" and "error" makes the state of the data request queryable at any time.
A critical difference: if useAsyncData fails on the server and error is not handled, Nuxt automatically triggers error.vue. Anyone who wants to prevent that must either set { server: false } or handle the error locally and call clearError(). The pattern of combining errors from useAsyncData with a local error boundary gives full control: server errors are caught by error.vue, client-side fetch errors after hydration by NuxtErrorBoundary. This layered error handling is the key to robust error boundary design in Nuxt.
8. Error monitoring and Sentry integration
Error handling without monitoring is blind. An error boundary in Vue that silently swallows errors without logging them creates a false sense of security. Sentry is the most widely used monitoring tool for Vue and Nuxt applications and can be integrated via the official Nuxt module @sentry/nuxt. After integration, Sentry automatically catches all unhandled errors, including source-map deobfuscation for minified code. You see exactly which line of the original code the error occurred in.
Combined with error boundaries, a clear reporting pattern emerges: errors caught by a local boundary are manually reported via Sentry.captureException() and marked as "handled". Errors that fall through all boundaries and are caught by the global handler or the Nuxt system land in Sentry as "unhandled". This distinction immediately gives the team priority: "unhandled" errors are critical and must be fixed right away, "handled" errors are known edge cases that should be monitored. Anyone who uses error boundaries in Vue and Nuxt without this monitoring gives away the most valuable part: knowing which errors actually occur in production.
9. Comparing the error handling layers
Vue 3 and Nuxt 3 offer various mechanisms for error boundaries and error handling that differ in scope, granularity and use case. Choosing the right mechanism depends on how much of the UI tree needs to be isolated and whether it is an error in the render phase, a lifecycle hook, or an asynchronous data fetch.
| Mechanism | Level | Catches | Recommended use |
|---|---|---|---|
| onErrorCaptured | Component | Render, lifecycle, watcher | Widget-level isolation |
| app.config.errorHandler | Application | All uncaught errors | Monitoring & logging |
| error.vue (Nuxt) | Route / page | 404, 500, SSR errors | Fatal page errors |
| NuxtErrorBoundary | Section | Async errors in slot | Granular error isolation |
| useFetch error ref | Composable | HTTP and network errors | Declarative data error handling |
In practice, all five mechanisms are combined. A typical setup: NuxtErrorBoundary for API-dependent widgets, onErrorCaptured in critical parent components for render errors, error.vue as a page-level fallback for unknown routes and server errors, and app.config.errorHandler as a universal logging hook. This layering ensures that no error goes unnoticed while keeping the smallest possible part of the application unavailable to the user.
Mironsoft
Vue 3 and Nuxt 3 development with robust error handling
Vue applications that stay usable even when errors occur?
We analyze existing Vue and Nuxt projects for missing error boundaries, implement granular error isolation and integrate monitoring that makes production errors visible before users report them.
Error analysis
Audit of existing Vue applications for missing error boundaries and unhandled async errors
Implementation
Clean error boundary architecture with NuxtErrorBoundary, onErrorCaptured and error.vue
Monitoring
Sentry integration with source maps, structured logging and error prioritization
10. Summary
Robust error boundaries in Vue and Nuxt are not a luxury but a basic requirement for production-ready applications. onErrorCaptured isolates errors at the component level and prevents a widget error from destroying the entire page. The global app.config.errorHandler is the last line of defense for all errors that have no local boundary, and the right place for structured logging and monitoring integration. In Nuxt 3, error.vue for fatal page errors and NuxtErrorBoundary for granular section isolation complete the picture.
The interplay of these mechanisms creates a layered safety net: errors are caught at the lowest possible level, the user is presented with a contextual fallback UI, and the team gets the information it needs for a fast diagnosis through monitoring. Anyone implementing error boundaries in Vue today is investing in user satisfaction and support efficiency at the same time.
Error Boundaries in Vue & Nuxt: The Essentials at a Glance
Local isolation
onErrorCaptured in parent components catches render and lifecycle errors from the entire child tree. Returning false stops propagation.
Global logging
app.config.errorHandler is the last line of defense, ideal for Sentry integration and structured error monitoring in production.
Nuxt layers
error.vue for fatal page errors (404, 500, SSR). NuxtErrorBoundary for granular widget isolation without a page change.
Async errors
useFetch and useAsyncData deliver errors in a ref, handled declaratively in the template, without try-catch in the component.