How to Use Error Handling to Create Rock Solid Nuxt Apps

This article explores preventing user frustrations in your Nuxt 3 application by building rock solid applications through error handling.

Michael Thiessen
Nuxt 3

Mastering Nuxt 3 course is here!

Get notified when we release new tutorials, lessons, and other expert Nuxt content.

Click here to view the Nuxt 3 course

It really sucks when an app stops working properly, for no apparent reason!

And it really sucks when this happens to your app, to your users — who then start to complain and get frustrated.

The way to prevent this is by putting in robust error handling in your app.

In this article we’ll cover it all:

  • The basics of error handling in Nuxt 3
  • Global errors vs client-side errors
  • How to create custom error pages in Nuxt
  • Handling errors on server routes (API endpoints)
  • Handling errors in Vue components, whether server-rendered or not
  • Handling errors that happen in our composables and other non-component code
  • Dealing with client-side errors using NuxtErrorBoundary
  • Handling errors in middleware, both route middleware and server middleware

Okay, there may be a few more places in your Nuxt app that errors can happen, but I think we’ve covered most of it here!

Error Handling Basics

Before we look at how to handle errors in more specific situations, let’s cover the error handling tools that Nuxt gives us.

If something unexpected happens, we can throw an error using createError:

throw createError({
  statusCode: 500,
  statusMessage: 'Something bad happened on the server',
});

This method can be used anywhere — in Vue components, server routes, or anywhere else, really.

Once an error has been thrown, either by us or some other code, we can inspect the error using the useError composable:

<script setup>
const error = useError();
</script>

Knowing what the error is lets us create custom error pages and figure out how to handle the error:

<template>
  <NuxtLayout>
    <div class="prose">
      <h1>Dang</h1>
            <p>
              <strong>{{ error.message }}</strong>
            </p>
      <p>It looks like something broke.</p>
      <p>Sorry about that.</p>
    </div>
  </NuxtLayout>
</template>

We can also clear the errors to “reset” our app using the clearError method. We’ll often use the redirect option so we can navigate to somewhere that is (hopefully!) not broken:

clearError({ redirect: '/dashboard' });

Lastly, we have the NuxtErrorBoundary component which lets us handle client-side errors in a really nice way:

<NuxtErrorBoundary>
    <VideoPlayer />
    <template #error="{ error }">
        <div>
            <p>Oops, it looks like the video player broke :/</p>
            <p>{{ error.message }}</p>
        </div>
    </template>
</NuxtErrorBoundary>

I wrote an in-depth article on these tools here if you’re not familiar with these.

Server (or Global) Errors vs. Client Errors

In order to understand how to handle errors in our Nuxt apps, we also need to understand the different types of errors we can get.

In Nuxt we have two types of errors:

  • Global errors: these errors can be thought of as “server-side” errors, but they’re still accessible from the client
  • Client-side errors: these errors only exist on the client, so they don’t affect the rest of your app and won’t show up in logs unless you use a logging service like LogRocket (there are many of these services out there).

It’s important to understand this distinction, because these errors happen under different circumstances and need to be handled differently.

Global errors can happen any time the server is executing code. Mainly, this is during an API call, during a server-side render, or any of the code that glues these two together.

Client-side errors mainly happen while interacting within an app, but they can also happen on route changes because of how Nuxt’s Universal Rendering works.

It’s important to note that the NuxtErrorBoundary component only deals with client-side errors, and does nothing to handle or clear these global errors. We’ll see how this works a bit later in the article.

Displaying Custom Error Pages

With Nuxt 3 we can pretty easily create our own custom error pages.

We have a few steps to do this:

  1. The error is thrown. Either by our own code, or some other code, like the Nitro server.
  2. Our error.vue page is rendered.
  3. On that page, we then need to use useError to grab the error and inspect it.
  4. We can use these details to craft a more specific error page, by showing a different message or different UI altogether based on the type of error.
  5. Finally, we’ll use the clearError method to give the user a way to recover from the error and keep using our app.

Here’s an example that incorporates all five of these steps:

<!-- 2️⃣ The error.vue page is rendered -->
<template>
  <NuxtLayout>
    <div v-else class="prose">
      <!-- 4️⃣ We use the status code to determine what to show -->
        <template v-if="error.statusCode === 404">
          <h1>404!</h1>
          <p>Sorry, that page doesn't exist.</p>
        </template>
            <template v-else>
          <h1>Dang</h1>
                <p>
          <!-- 4️⃣ We can also show the actual error message -->
                  <strong>{{ error.message }}</strong>
                </p>
          <p>It looks like something broke.</p>
          <p>Sorry about that.</p>
      </template>
      <p>
            Go back to your
        <!-- 5️⃣ Handle the error for the user --> 
            <a @click="handleError">
              dashboard.
            </a>
          </p>
    </div>
  </NuxtLayout>
</template>
<script setup>
// 3️⃣ We grab the current global error to inspect it
const error = useError();

// 5️⃣ Use the clearError method to clear the error and
// redirect back to the app.
const handleError = () => {
  clearError({
    redirect: '/dashboard',
  });
};
</script>

This is a quick overview, but I wrote more in-depth about custom error pages in Nuxt here. TK link to article here.

Handling Errors in Server Routes

Within our server routes — also called event handlers — we can throw and handle errors in our JavaScript.

If these errors are not caught, they become global errors in the Nuxt app, and will cause an error page to be thrown.

But, we can catch these errors before the user ever notices them.

Take a look at this endpoint we created in the article on modifying our database data with Prisma. Specifically, look at the part halfway through where we use createError:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Endpoint that updates the progress of a lesson
export default defineEventHandler(async (event) => {
  // Only allow PUT, PATCH, or POST requests
  assertMethod(event, ['PUT', 'PATCH', 'POST']);

    // Throw a 401 if there is no user logged in.
  protectRoute(event);

  // Get the route params
  const { chapterSlug, lessonSlug } = event.context.params;

  // Get the lesson from the DB
  const lesson = await prisma.lesson.findFirst({
    where: {
      slug: lessonSlug,
      Chapter: {
        slug: chapterSlug,
      },
    },
  });

  // If the lesson doesn't exist, throw a 404
  if (!lesson) {
    // 👇 Here we throw an error because something unexpected happened 👇
    throw createError({
      statusCode: 404,
      statusMessage: 'Lesson not found',
    });
  }

  // Get the completed value from the request body and update progress in DB
  // Select based on the chapter and lesson slugs
  const { completed, userEmail } = await readBody(event);
  // Get user email from the supabase user if there is one.
  const {
    user: { email: userEmail },
  } = event.context;
  return prisma.lessonProgress.upsert({
    where: {
      lessonId_userEmail: {
        lessonId: lesson.id,
        userEmail,
      },
    },
    update: {
      completed,
    },
    create: {
      completed,
      userEmail,
      Lesson: {
        connect: {
          id: lesson.id,
        },
      },
    },
  });
});

Later on in the article, when we go to use this endpoint, we wrap it in a try...catch block just in case this error is thrown. Ideally, we never show an error page to the user, but instead we catch them and do something to resolve it automatically:

try {
  await $fetch(
    `/api/course/chapter/${chapter}/lesson/${lesson}/progress`,
    {
      method: 'POST',
      // Automatically stringified by ofetch
      body: {
        completed: !currentProgress,
        userEmail: user.value.email,
      },
    }
  );
} catch (error) {
  console.error(error);

  // Any other logic needed to recover from a failed update
}

But this isn’t the only place where we should be mindful of catching errors.

In the endpoint we call Prisma to make an update to our database. There are any number of things that could go wrong here, since we’re dealing with an external service and a database that’s constantly changing.

Here, too, we should wrap this code in a try...catch block:

// Get the completed value from the request body and update progress in DB
// Select based on the chapter and lesson slugs
const { completed, userEmail } = await readBody(event);
// Get user email from the supabase user if there is one.
const {
  user: { email: userEmail },
} = event.context;
let result;

try {
  // 👇 Wrap this error-prone code so we can intercept any errors
  result = prisma.lessonProgress.upsert({
    where: {
      lessonId_userEmail: {
        lessonId: lesson.id,
        userEmail,
      },
    },
    update: {
      completed,
    },
    create: {
      completed,
      userEmail,
      Lesson: {
        connect: {
          id: lesson.id,
        },
      },
    },
  });
} catch (e) {
  // Retry the request, log the error, or simply return a more
  // descriptive error message here
}

return result;

Handling Rendering Errors in Vue Components

With Universal Rendering our Nuxt apps have two places where rendering errors can occur — on the server during server-side rendering, and on the client.

Dealing with errors here is no different than what we’ve already seen, regardless of the type of component. Whether it’s a page, layout, or component, we use createError and useError to handle them:

<script setup>
const someMethod = () => {
  // ...
    
  try {
    // Some error-prone code
  } catch (e) {
    console.error(e);  // Or use a logging service like LogRocket

    throw createError({
      statusCode: 404,
      statusMessage: 'We could not find that type of hotdog, sorry.'
    });
  }

  // ...
}
</script>

Remember, we want to wrap any error-prone code in a try...catch block so we can properly redirect and manage the error instead of just letting it crash our app.

Handling Errors in Composables

But what if we encounter an error and we’re not inside of a component?

The approach is the same! It’s just JavaScript we’re working with (or TypeScript).

I’ll show you what I mean.

Here’s the composable from an article I wrote on making a version of useFetch that has caching:

// ~/composables/useFetchWithCache.ts

import { StorageSerializers } from '@vueuse/core';

export default async <T>(url: string) => {
  // Use sessionStorage to cache data
  const cached = useSessionStorage<T>(url, null, {
    serializer: StorageSerializers.object,
  });

  if (!cached.value) {
    const { data, error } = await useFetch<T>(url);

    if (error.value) {
      // 👇 Using createError again to craft our own error
      throw createError({
        ...error.value,
        statusMessage: `Could not fetch data from ${url}`,
      });
    }

    // Update the cache
    cached.value = data.value as T;
  } else {
    console.log(`Getting value from cache for ${url}`);
  }

  return cached;
};

You maybe noticed that we’re not using a try...catch block here.

That’s because Nuxt does this error handling for us in useFetch. Because it returns both a data and a error object, we can simply check to see if error has been set or not:

const { data, error } = await useFetch<T>(url);

if (error.value) {
  // 👇 Using createError again to craft our own error
  throw createError({
    ...error.value,
    statusMessage: `Could not fetch data from ${url}`,
  });
}

Then, we pass along the error’s status code to our message, but we could decide to put in a specific code if that makes more sense.

Handling Route Middleware Errors

If we’re protecting our routes using auth, it’s possible that something will go wrong — auth is complicated!

Dealing with errors properly in Nuxt 3 route middleware is (thankfully!) not that different from what we’ve seen.

But, there is a slight twist.

Let’s take this example route middleware:

export default defineNuxtRouteMiddleware((to, from) => {
    // We can use the `to` and `from` routes to figure
  // out what we should be doing in this middleware 

  if (notValidRoute(to)) {
        // Shows a "Page not found" error by default
    return abortNavigation();
  } else if (useDifferentError(to)) {
        // Pass in a custom error
      // 👇 We need to wrap createError with abortNavigation
    return abortNavigation(
      createError({
        statusCode: 404,
        message: 'The route could not be found :(',
      })
    );
  } else if (shouldRedirect(to)) {
        // Redirect back to the home page
    return navigateTo('/');
  } else {
    // If everything looks good, we won't do anything
    return;
  }
});

In route middleware we can’t simply throw an error, we have to pass it to the special abortNavigation method, and then return the Promise from that method instead:

return abortNavigation(
  createError({
    statusCode: 404,
    message: 'The route could not be found :(',
  })
);

The [abortNavigation method](https://nuxt.com/docs/api/utils/abort-navigation) can be called in a few different ways:

  1. No parameters abortNavigation() — this will create a 404 and Page not found error page
  2. Just an error message abortNavigation('Whooooops!') — allows us to set a more descriptive error message
  3. Custom error object abortNavigation(error) — this lets us customize exactly what error is used. We can either get this error object from somewhere else (like a try...catch block) or by using the createError method.

Returning abortNavigation will stop the navigation that is currently taking place, and instead redirect to an error page.

Handling Server Middleware Errors

In our series on adding auth to a Nuxt 3 app, we added this server middleware to protect our sensitive endpoints:

// ~/server/middleware/auth.js

import { serverSupabaseUser } from '#supabase/server';

export default defineEventHandler(async (event) => {
  const user = await serverSupabaseUser(event);

  // 👇 Throw an error to prevent the request from continuing
    if (!user) {
    throw createError({
      statusCode: 401,
      message: 'Unauthorized',
    });
  }
});

Here, we’re throwing an error using createError when the user is not defined. This prevents the request from continuing, and immediately returns a 401 error back to whoever is calling this endpoint.

However, this is run for all requests to all endpoints.

Server middleware in Nuxt cannot be scoped to specific endpoints or routes. But there is a simple way to solve this so we can protect only certain routes.

When do we throw an error?

It’s possible to wrap every thing in your code with a try...catch block, and throw errors left and right for every possible thing that could go wrong in your application.

Getting this balance right can be tricky, and nothing can replace hard-won experience here.

But the main question is this:

Where are the errors likely to happen?

I’d give a brief list:

  • Calling external APIs — networks are flaky and other people’s systems break, too
  • Using third-party libraries
  • Any sufficiently complex code
  • User interactions and input — users tend to do things we never anticipated
Michael Thiessen
Michael is a passionate full time Vue.js and Nuxt.js educator. His weekly newsletter is sent to over 11,000 Vue developers, and he has written over a hundred articles for his blog and VueSchool.

Follow MasteringNuxt on