This page is a focused follow-up to Next.js & Nuxt Routing Error Pages, which covers routing-level error page strategy across both frameworks.
The exact failure this page solves #
A Nuxt 3 page throws during data fetching, a plugin fails to initialize, or a nested component raises during render, and instead of a graceful in-page fallback the entire app unmounts and Nuxt falls back to its built-in error screen. Without a project-level error.vue, users see a bare, unstyled error page with no way back into the app, and every distinct failure — a 404 from a bad route param, a 500 from a failed API call, an unexpected client exception — gets the exact same treatment. The fix is a single root error.vue that inspects the NuxtError it receives, renders a status-appropriate message, and gives the user an explicit recovery action built on clearError().
This is narrower than the general question of where to put routing error boundaries; it assumes you already have error.vue present and need it to behave correctly for both intentionally raised errors and genuinely unexpected ones, in both server-rendered and client-only contexts.
Zero-to-working implementation #
Place this file at the project root as error.vue, alongside app.vue. Nuxt auto-detects it and renders it in place of the normal app tree whenever an unhandled or fatal: true error reaches the top level.
<template>
<div class="error-page">
<h1>{{ heading }}</h1>
<p>{{ error.statusMessage || 'An unexpected error occurred.' }}</p>
<pre v-if="isDev && error.stack" class="stack">{{ error.stack }}</pre>
<button type="button" @click="handleRecover">Back to safety</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { NuxtError } from '#app'
// Nuxt injects this prop automatically when rendering error.vue.
const props = defineProps<{ error: NuxtError }>()
const isDev = import.meta.dev
const heading = computed(() => {
const code = props.error.statusCode
if (code === 404) return 'Page not found'
if (code && code >= 500) return 'Something broke on our end'
return 'Something went wrong'
})
function handleRecover() {
// Clears the reactive error state and sends the user to a known-good route.
clearError({ redirect: '/' })
}
</script>
<style scoped>
.error-page {
display: grid;
gap: 1rem;
place-items: center;
min-height: 60vh;
text-align: center;
padding: 2rem;
}
.stack {
max-width: 100%;
overflow-x: auto;
text-align: left;
font-size: 0.8rem;
}
</style>
error.vue is auto-imported and requires no manual registration. It has access to the same auto-imported composables — useError(), clearError(), useRoute() — as any other component, and defineProps is the only mandatory boilerplate: Nuxt always passes the error prop typed as NuxtError, a superset of the standard Error shape with statusCode, statusMessage, fatal, unhandled, and data.
Step-by-step explanation #
-
error.vueat the root. Nuxt scans the project root for this filename and, when present, swaps it in forapp.vuewhenever a fatal error surfaces. It is not nested inside your usual layout system, so it must be fully self-contained. -
createError({ statusCode, statusMessage, fatal: true }). Call this inside a page’ssetup(), a route middleware, or a server API handler to raise a controlled failure. Thefatal: trueflag is what tells Nuxt’s client-side runtime to escalate the error to the top-levelerror.vuerather than letting it bubble as an ordinary thrown exception. -
useError()for reading state anywhere. This composable returns the same reactiveRef<NuxtError | null>thaterror.vuereceives as a prop, so a header component can, for example, show a banner while an error is active without importingerror.vuedirectly. -
clearError({ redirect: '/' })to recover. Calling this from a button handler resets the internal error ref tonulland, when aredirectoption is supplied, performs a client-side navigation to that route. Without aredirect, the user stays on the current URL with the error simply cleared. -
SSR vs client rendering of the error page. If the error is thrown during server-side rendering,
error.vueis rendered on the server and sent down as part of the initial HTML — there is no flash of the normal app. If the error occurs after hydration (a client-only exception in an event handler, for instance), Nuxt tears down the mounted app and swaps inerror.vueclient-side; guard anything that assumes a browser API withimport.meta.clientsince the same component can, in principle, still be reached via SSR on a subsequent request.
Edge cases #
| Scenario | Behavior | Mitigation |
|---|---|---|
fatal: false (default) thrown client-side, post-hydration |
Error does not replace the page; it propagates through Vue’s normal error handling instead | Only set fatal: true when you deliberately want the full error.vue takeover; otherwise catch it locally with onErrorCaptured or a try/catch |
createError thrown during SSR |
Always reaches the client as a rendered error.vue, fatal flag or not, because the response never completed as a normal page |
Set statusCode deliberately (404, 500, etc.) so error.vue can branch on it even though fatal was never explicitly true |
clearError() called without state cleanup |
The error clears and the user navigates away, but any store or composable state left mid-mutation persists | Reset the relevant Pinia store or composable inside the same handler that calls clearError, before or after the call as your flow requires |
Hydration of error.vue itself |
If SSR rendered error.vue for one error, then a client-side navigation triggers a second, different error before the first ever hydrates, error.stack and data can diverge from what a fresh client-only render would produce |
Treat error.vue as idempotent: derive all UI purely from the current error prop rather than caching values from a previous render pass |
The fatal flag and the SSR/client split interact in a way that trips up most first attempts: on the server, “fatal” is effectively always true in practice, because there is no partially-rendered app to fall back to — the whole response becomes the error page. On the client, fatal is the only signal Nuxt has that you want the full-page takeover instead of a locally handled exception, which is why omitting it is the single most common reason error.vue “doesn’t fire” for an error that clearly happened.
Verification steps #
-
Force a fatal error from a page. In any page component, add
throw createError({ statusCode: 500, statusMessage: 'Forced failure', fatal: true })insidesetup()or anonMountedhook, then load the route. Confirm the browser renders your customerror.vuelayout, not Nuxt’s default error screen, and that the heading logic branches correctly for a500. -
Confirm SSR rendering. With JavaScript disabled in DevTools (or via
curlagainst the dev server), request the same route directly. The response HTML should already contain theerror.vuemarkup — proof the error was caught during server-side rendering, not just patched in after hydration. -
Confirm recovery. Click the “Back to safety” button and verify, via the Vue DevTools or a
console.loginuseError()'s consumer, that the error ref becomesnulland the browser navigates to/. -
Check
useError()reactivity elsewhere. Add auseError()call in a layout or header component, force the same fatal error, and confirm that component’s reactive read reflects the sameNuxtErrorinstanceerror.vuereceived — not a stale or undefined value. -
Non-fatal path. Repeat step 1 with
fatal: false(or omit the flag) from client-only code after the page has hydrated, and confirmerror.vuedoes not take over the page — the exception should instead be visible in the console or caught by a localonErrorCaptured, demonstrating the fatal/non-fatal split is working as intended.
Frequently asked questions #
Does a non-fatal createError still render error.vue?
During server-side rendering, any uncaught createError reaches the client as a rendered error.vue regardless of the fatal flag, because the response never completed normally. After hydration, on the client, only errors thrown with fatal: true switch the app into the full error.vue state; a non-fatal error thrown from client-only code is instead surfaced through Vue’s own error propagation and does not replace the page.
Does clearError reset my Pinia store or composable state?
No. clearError only resets the reactive error object returned by useError() back to null and performs the optional redirect. Any store or composable state that existed before the crash is untouched, so if the failure left data in an inconsistent shape you must reset it explicitly, typically in the same handler that calls clearError.
Can error.vue use my normal app layout and navigation?
Not automatically. error.vue replaces the entire app.vue tree, including NuxtLayout and NuxtPage, so none of your registered layouts apply. If you want a header or navigation on the error page, import and render those components directly inside error.vue.
Related #
- Next.js & Nuxt Routing Error Pages — parent guide covering routing-level error page strategy across both frameworks
- Using global-error.tsx to Catch Root Layout Crashes in Next.js — the Next.js equivalent of a root-level fatal error takeover
- Vue & Svelte Global Error Handlers — app-wide error capture for Vue components outside the routing layer