Error Pages, Fallbacks and Error Boundaries in Vue & Nuxt
AI generated
<v/>
{ }
Vue 3 · Nuxt 3 · Error Boundaries · Error Handling
Error Pages, Fallbacks and
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.

12 min read onErrorCaptured · errorHandler · error.vue · NuxtErrorBoundary Vue 3.4+ · Nuxt 3.x

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.

11. FAQ: Error Boundaries in Vue & Nuxt

1What is an error boundary in Vue 3?
A component with onErrorCaptured that catches errors from the entire child tree and shows a fallback UI instead of the crashed area, without affecting the rest of the application.
2What does onErrorCaptured NOT catch?
Errors in asynchronous event handlers outside the Vue lifecycle, e.g. in setTimeout or native DOM events. These need try-catch or window.onerror.
3error.vue vs. NuxtErrorBoundary?
error.vue replaces the entire page on fatal errors. NuxtErrorBoundary isolates only a page area and keeps navigation and other widgets functional.
4Integrating Sentry into an error boundary?
Call Sentry.captureException(error) in onErrorCaptured or the global errorHandler. @sentry/nuxt sets up the integration automatically, source maps enable deobfuscation.
5onErrorCaptured in the Composition API?
Yes, import { onErrorCaptured } from 'vue' and call it in script setup or setup(), a full-fledged lifecycle hook of the Composition API.
6What does return false in onErrorCaptured do?
Stops error propagation, parent components and the global errorHandler do not see the error. Without return false, the error keeps propagating upward.
7Handling errors in useFetch?
useFetch returns an error ref. In the template, use v-if="error" for a fallback UI and retry button, no try-catch needed in the component.
8Catching SSR errors without error.vue?
{ server: false } in useAsyncData shifts the fetch to the client. Alternatively handle the error locally and call clearError() before it reaches Nuxt.
9NuxtErrorBoundary for render errors?
NuxtErrorBoundary is primarily for async errors from Nuxt composables. For synchronous render errors in child components, onErrorCaptured is more reliable.
10Reusable ErrorBoundary component?
onErrorCaptured plus a reactive hasError ref plus named slots for normal content and fallback. resetError resets hasError, without a page reload.