Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 048 / 100

Error handling in Nuxt: a practical guide for developers

Graceful errors everywhere.

Expona Admin7 min read

Error handling in Nuxt: a practical guide for developers

Error handling in Nuxt isn't an afterthought—it's the difference between users seeing a cryptic crash and a thoughtfully designed experience. We've all shipped code that breaks in production, and how we handle those failures defines the quality of our applications. In this guide, we'll walk through the practical techniques that keep your Nuxt applications resilient and your error messages meaningful.

Why error handling matters in Nuxt

Nuxt runs on both server and client, which means errors can happen in different contexts and need different approaches. A database query failing on the server is different from a missing API response on the client. Without proper error handling, users bounce to blank pages or see console errors that mean nothing to them. When we implement error handling in Nuxt the right way, we catch these issues early, log them for debugging, and show users something useful.

The challenge is that error handling in Nuxt spans multiple layers: route middleware, server routes, API calls, and component logic. Each layer needs its own strategy, but they should work together in a cohesive system.

createError in server routes

Server routes are where many errors originate. A database connection fails, an external API times out, or validation rejects user input. The createError helper is your tool for creating Nuxt-aware error objects that propagate gracefully to your error page.

Let's say we're building a user profile endpoint. The user might not exist, the database might be down, or the request might be malformed. Here's how we handle it:

typescript
// server/api/user/[id].get.ts
export default defineEventHandler(async (event) => {
  const userId = getRouterParam(event, 'id')
  
  // Validate the ID format
  if (!userId || !/^\d+$/.test(userId)) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Invalid user ID format',
      data: {
        message: 'User ID must be a positive integer'
      }
    })
  }
  
  try {
    const user = await getUserFromDatabase(userId)
    
    if (!user) {
      throw createError({
        statusCode: 404,
        statusMessage: 'User not found',
        data: {
          message: `No user exists with ID ${userId}`
        }
      })
    }
    
    return user
  } catch (error) {
    // If it's already a Nuxt error, re-throw it
    if (error instanceof H3Error) {
      throw error
    }
    
    // Otherwise, catch unexpected errors and expose them safely
    console.error('Database error:', error)
    throw createError({
      statusCode: 500,
      statusMessage: 'Internal server error',
      data: {
        message: 'Something went wrong. Please try again later.'
      }
    })
  }
})

Notice that we're being specific about status codes and messages. A 400 tells the client it sent bad data. A 404 tells them the resource doesn't exist. A 500 tells them something internal broke. This specificity helps both debugging and client-side error handling. We also hide the actual error message from the user in the 500 case, logging it server-side instead.

The key principle: use createError for expected error conditions, and wrap unexpected errors in a safe 500 response.

error.vue for custom error pages

When an error is thrown—whether from a server route, middleware, or component—Nuxt looks for an error.vue file to display it. This is your chance to make errors user-friendly instead of terrifying.

Create this file at the root of your app:

typescript
// error.vue

  <div>
    <h1>{{ error?.statusCode || 'Error' }}</h1>
    <p>{{ error?.statusMessage || 'Something went wrong' }}</p>
    <p>
      {{ error.data.message }}
    </p>
    
    <div>
      Back to home
      Try again
    </div>
  </div>




This error page receives the error object and displays relevant information. For a 404, users see a friendly message and options to navigate. For a 500, they see a reassuring message without technical details. The page also provides actions—going home or trying again—which reduces frustration.

showError and clearError

Beyond server routes, errors happen in the browser too. An API call fails, a computation throws, or validation rejects. The showError composable lets us trigger the error page from anywhere, and clearError dismisses it.

In a component or middleware, use them like this:

typescript
// composables/useUserFetch.ts
export const useUserFetch = () => {
  const user = ref(null)
  const { showError } = useError()
  
  const fetchUser = async (id: string) => {
    try {
      const response = await $fetch(`/api/user/${id}`)
      user.value = response
    } catch (error) {
      console.error('Fetch failed:', error)
      
      // Show the error page with Nuxt's error handling
      showError({
        statusCode: 500,
        statusMessage: 'Failed to load user',
        data: {
          message: 'Unable to fetch user data. Please refresh the page.'
        }
      })
    }
  }
  
  return { user, fetchUser }
}

And in a component:

typescript
// pages/users/[id].vue



  <div>
    <div>
      <h1>{{ user.name }}</h1>
      <p>{{ user.email }}</p>
    </div>
    Retry
  </div>

The pattern here is straightforward: catch errors from async operations, log them, and show the error page. clearError with redirect: false lets you retry without navigation. This gives users agency and makes your app feel less fragile.

Log errors to a monitoring service

Displaying errors to users is half the battle. The other half is knowing what went wrong so we can fix it. Monitoring services like Sentry, LogRocket, or DataDog catch errors in production and alert us.

Set up error logging in a plugin:

typescript
// plugins/error-monitoring.client.ts
export default defineNuxtPlugin((nuxtApp) => {
  // Initialize your monitoring service
  if (process.client) {
    initializeErrorTracking()
  }
  
  nuxtApp.hook('app:error', (error) => {
    // Send all errors to monitoring service
    captureException(error, {
      tags: {
        source: 'nuxt-app'
      }
    })
  })
  
  // Catch unhandled promise rejections
  if (process.client) {
    window.addEventListener('unhandledrejection', (event) => {
      captureException(event.reason, {
        tags: {
          source: 'unhandled-rejection'
        }
      })
    })
  }
})

const captureException = (error: any, context: Record) => {
  // Example: send to Sentry
  if (window.$sentry) {
    window.$sentry.captureException(error, { contexts: context })
  }
}

For server-side errors, use a similar approach in a server plugin:

typescript
// server/plugins/error-monitoring.ts
export default defineNuxtPlugin(() => {
  // Initialize server-side monitoring
  if (process.server) {
    initializeServerErrorTracking()
  }
})

const handleServerError = (error: any, context: Record) => {
  // Send to monitoring service
  logErrorToService({
    message: error.message,
    stack: error.stack,
    context,
    timestamp: new Date().toISOString()
  })
}

With error logging in place, you'll receive alerts when things break in production. You can track error frequency, identify patterns, and prioritize fixes. This transforms your error handling from reactive (users complain) to proactive (you know before they do).

Common mistakes

  • Swallowing errors silently: Don't catch errors without logging or showing them. Silent failures make debugging impossible. Always log, always inform the user.
  • Exposing sensitive details: Never show database queries, API keys, or internal paths to users. Log these server-side; show generic messages to clients.
  • Not distinguishing error types: Handle expected errors (validation, not found) differently from unexpected ones (database crash, external API down). This helps you debug faster and respond more intelligently.
  • Ignoring error boundaries: Errors in deeply nested components can crash the whole page. Use error boundaries or error states in composables to contain failures.
  • Forgetting about network errors: API calls can fail for timeouts, network loss, or server unavailability. Always have a fallback strategy for network errors.

Key takeaways

  • Use createError in server routes to create Nuxt-aware errors with proper status codes and messages.
  • Create an error.vue page that turns errors into user-friendly experiences with actionable options.
  • Use showError and clearError composables to handle errors in client-side code and give users retry options.
  • Log all errors to a monitoring service so you know what's breaking in production before users tell you.
  • Distinguish between expected errors (validation, not found) and unexpected ones (crashes, timeouts), and handle each appropriately.
  • Always hide sensitive details from users while logging them server-side for debugging.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles