Skip to content

Nuxt Error Handling — Custom Error Pages, Error Boundaries, and API Error Handling

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Nuxt Error Handling. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn Nuxt error handling — create custom 404 and 500 error pages, implement error boundaries, handle API errors gracefully, and display user-friendly error messages.

In this lesson, you'll understand how to handle errors in Nuxt at every level — pages, API routes, components, and server-side — with proper user feedback and logging.

What You'll Learn

How to create custom error pages, use createError and showError for programmatic error handling, implement error boundaries with NuxtErrorBoundary, handle API errors in server routes, and log errors for debugging.

Why It Matters

Users encounter errors — broken links, network failures, server crashes — and how your application responds determines whether they stay or leave. Good error handling turns frustration into a guided recovery experience.

Real-World Use

An e-commerce platform with 10,000+ products uses custom Nuxt error pages and error boundaries so that when a product API fails, only the product card shows an error state instead of crashing the entire page.

flowchart TD
    A[Error Occurs] --> B{Error Type}
    B -->|404 Not Found| C[Custom 404 Page]
    B -->|500 Server Error| D[Custom 500 Page]
    B -->|Component Error| E[NuxtErrorBoundary]
    B -->|API Error| F[Error Response]
    C --> G[User-friendly Message]
    D --> G
    E --> H[Fallback UI]
    F --> I[Structured Error JSON]
    style A fill:#00dc82,color:#fff

Custom Error Pages

Create error.vue in the project root to handle all errors:

<template>
  <div class="error-page">
    <div class="error-content">
      <h1>{{ error.statusCode }}</h1>
      <p>{{ error.message || 'An unexpected error occurred' }}</p>
      <p class="error-description">
        <template v-if="error.statusCode === 404">
          The page you're looking for doesn't exist or has been moved.
        </template>
        <template v-else-if="error.statusCode === 500">
          Something went wrong on our end. Please try again later.
        </template>
        <template v-else>
          We encountered an issue. Please go back or try again.
        </template>
      </p>
      <div class="error-actions">
        <NuxtLink to="/" class="btn btn-primary">
          Go Home
        </NuxtLink>
        <button @click="handleError" class="btn btn-secondary">
          Try Again
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  error: Object
});

const handleError = () => clearError({ redirect: '/' });
</script>

Expected output: A styled error page that shows the status code, a human-readable message, and action buttons based on the error type.

Programmatic Error Throwing

Use createError to throw errors in pages and composables:

<script setup>
const route = useRoute();

const { data: product } = await useAsyncData('product', () => {
  return $fetch(`/api/products/${route.params.id}`);
});

// Handle missing data
if (!product.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Product Not Found',
    message: `No product found with ID "${route.params.id}"`,
    fatal: true // Show error page
  });
}

// Handle validation errors
if (route.params.id.length < 3) {
  throw createError({
    statusCode: 400,
    statusMessage: 'Bad Request',
    message: 'Product ID must be at least 3 characters'
  });
}
</script>

<template>
  <div class="product-page" v-if="product">
    <h1>{{ product.name }}</h1>
    <p>{{ product.description }}</p>
  </div>
</template>

Expected output: Missing products show a 404 error page. Invalid IDs show a 400 error. Both display user-friendly messages without crashing the application.

NuxtErrorBoundary

Wrap components that might fail to prevent full-page crashes:

<template>
  <div class="dashboard">
    <h1>Dashboard</h1>

    <NuxtErrorBoundary @error="logError">
      <template #error="{ error }">
        <div class="error-fallback">
          <p>Failed to load user profile.</p>
          <button @click="resetError">Retry</button>
          <details>
            <summary>Technical details</summary>
            <pre>{{ error.message }}</pre>
          </details>
        </div>
      </template>

      <UserProfile :user-id="userId" />
    </NuxtErrorBoundary>

    <NuxtErrorBoundary>
      <template #error>
        <p class="fallback">Analytics widget unavailable.</p>
      </template>

      <AnalyticsWidget />
    </NuxtErrorBoundary>
  </div>
</template>

<script setup>
function logError(error) {
  console.error('Boundary caught:', error);
  // Send to error tracking service
}
</script>

Expected output: If UserProfile or AnalyticsWidget throws an error, only that component shows the fallback — the rest of the dashboard continues working normally.

API Error Handling

Server-side API routes should return structured errors:

// server/api/products/[id].ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');

  if (!id || id.length < 3) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Bad Request',
      data: {
        field: 'id',
        reason: 'Product ID must be at least 3 characters',
        received: id
      }
    });
  }

  const product = await findProductById(id);

  if (!product) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Not Found',
      data: {
        reason: `No product exists with ID "${id}"`,
        suggestion: 'Check the product ID and try again'
      }
    });
  }

  return product;
});

Handle API errors on the client:

<script setup>
const route = useRoute();
const error = ref(null);

const { data: product } = await useAsyncData('product', async () => {
  try {
    return await $fetch(`/api/products/${route.params.id}`);
  } catch (e) {
    error.value = {
      status: e.response?.status || 500,
      message: e.response?._data?.statusMessage || 'Failed to load product',
      details: e.response?._data?.data || {}
    };
    return null;
  }
});
</script>

<template>
  <div v-if="error" class="error-state">
    <h2>Error {{ error.status }}</h2>
    <p>{{ error.message }}</p>
    <p v-if="error.details.suggestion">{{ error.details.suggestion }}</p>
    <NuxtLink to="/products">Back to Products</NuxtLink>
  </div>
  <div v-else-if="product" class="product">
    <h1>{{ product.name }}</h1>
  </div>
</template>

Expected output: API errors show contextual messages and suggestions instead of raw error text, helping users understand and resolve the issue.

Global Error Logging

Set up a plugin for centralized error tracking:

// plugins/error-logger.client.ts
export default defineNuxtPlugin({
  name: 'error-logger',
  setup(app) {
    // Vue error handler
    app.vueApp.config.errorHandler = (err, instance, info) => {
      console.error('[Vue Error]', err);
      console.error('[Component]', instance?.$options?.name);
      console.error('[Info]', info);

      // Send to error tracking service
      if (process.env.NODE_ENV === 'production') {
        fetch('/api/log-error', {
          method: 'POST',
          body: JSON.stringify({
            message: err.message,
            stack: err.stack,
            timestamp: new Date().toISOString(),
            url: window.location.href
          })
        }).catch(() => {});
      }
    };

    // Global promise rejection handler
    window.addEventListener('unhandledrejection', (event) => {
      console.error('[Unhandled Promise Rejection]', event.reason);
    });
  }
});

Expected output: All uncaught Vue errors and promise rejections are logged to the console in development and sent to an error tracking endpoint in production.

Common Mistakes

  1. Not handling errors in useAsyncData and useFetch: These composables set an error ref but don't automatically prevent rendering. Always check error.value before rendering data-dependent content.

  2. Showing raw error messages to users: Technical error messages (SQL errors, stack traces) confuse and frustrate users. Always map to user-friendly messages before displaying.

  3. Not providing fallback UI in NuxtErrorBoundary: Without the #error template, the boundary silently swallows errors without feedback. Always provide a fallback that tells the user something went wrong.

  4. Forgetting to clear errors after recovery: clearError() resets the error state. Without it, users stay on the error page even after the error condition is resolved.

  5. Not distinguishing between fatal and non-fatal errors: Use fatal: true for unrecoverable errors that should show the error page, and local error handling for recoverable component failures.

Practice Questions

  1. What is the difference between createError and showError? Answer: createError creates an error object you can throw. showError immediately displays the error page. Use createError with throw in pages and showError in middleware or plugins.

  2. How does NuxtErrorBoundary prevent full-page crashes? Answer: It catches errors thrown by its child components and renders the #error fallback instead. The rest of the page outside the boundary continues working.

  3. What information should a structured API error include? Answer: Status code, human-readable message, field-level validation details, suggestions for resolution, and optional debug information for developers.

  4. Why should you use clearError after handling an error? Answer: Without clearError, the error state persists and the error page continues showing. Call it to reset the error state and navigate back to normal content.

Challenge

Build a resilient data dashboard with: three independently loaded widgets wrapped in NuxtErrorBoundary, a custom 404 page with search suggestions, a custom 500 page with status check links, API routes that return structured errors with suggestions, a global error logger that captures all errors, and a retry mechanism for failed API calls using the error boundary's reset function.

Mini Project

Create an error-handled application with: custom 404 and 500 pages with branded styling, error boundaries around each major section (header, content, sidebar, footer), API routes with structured error responses including field validation details, a centralized error logging service, automatic retry for transient failures (network timeout, 503), and a user-facing error toast component for non-critical errors.

FAQ

What is the difference between `error.vue` and `~/error.vue`?

: The root error.vue catches all unhandled errors. Place it at the project root. Layout-level error pages are not supported — use NuxtErrorBoundary for component-level handling.

How do I test error pages in development?

: Navigate to a non-existent route for 404s. Use throw createError({ statusCode: 500 }) in a temporary page for 500s. Remove the test page after verification.

Can I have different layouts for error pages?

: Yes. The error.vue page can use layouts by wrapping content in <NuxtLayout>. Set a specific layout for error pages by calling setPageLayout('error') in the setup script.

How do I handle errors in server-side API routes?

: Use throw createError() in event handlers. The error is automatically serialized and sent as a JSON response with the correct status code and message.

What should I include in production error logs?

: Timestamp, error message, stack trace (sanitized), URL, user ID (not PII), browser/device info, and request context. Never log passwords, tokens, or personal data.

What's Next

Learn about Nuxt Testing to write unit tests, component tests, and end-to-end tests for your Nuxt application with Vitest and @nuxt/test-utils.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro