Handling Errors in Nuxt 3

This article explores errors you may face in building your Nuxt 3 application, and how you can handle these errors to make your application rock solid.

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

Throwing and handling errors is important for making any app rock solid in production.

Nuxt 3 makes this easy for us, and in this article I’ll show you exactly how.

We’ll cover:

  • How to throw errors from the back-end and the front-end
  • How to handle these errors well
  • The difference between global and client-side errors in Nuxt 3
  • How to use NuxtErrorBoundary to isolate client-side errors from infecting the rest of our application

It may not be the most interesting topic, but error handling is hugely important for building well-engineered software.

So let’s get to it!

Throwing Errors

To create an error, we’ll throw a Nuxt error that’s returned by the createError method:

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

The createError method is isomorphic, meaning it can be called on the server or on the client.

So you’ll use it inside of the Vue part of your app — components, composables, and Pinia stores:

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

export default async <T>(url: string) => {
  // Use sessionStorage to cache the lesson data
  const cached = useSessionStorage<T>(url, null, {
    // By passing null as default it can't automatically
    // determine which serializer to use
    serializer: StorageSerializers.object,
  });

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

      // 👇 Throw an error if the API isn't working
    if (error.value) {
      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;
};

(Learn more about creating this useFetchWithCache composable in this tutorial I wrote.)

As well as inside your Nitro event handlers in your API:

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

export default defineEventHandler(async (event) => {
  const { chapterSlug, lessonSlug } = event.context.params;

  const lesson = await prisma.lesson.findFirst({
    where: {
      slug: lessonSlug,
      chapter: {
        slug: chapterSlug,
      },
    },
  });

  // 👇 Throw an error if we can't find the lesson
  if (!lesson) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Lesson not found',
    });
  }

  return lesson;
});

(I wrote a whole series on using Prisma with Nuxt and Supabase if you want to learn more about that.)

But throwing errors is only half of the equation.

Handling Errors

Once errors are thrown, we know something broke. The next step is to actually deal with the problem — which also happens to be one of the hardest parts of being an adult…

To deal with errors we’ll employ three basic steps:

  1. Get more info on the error using the useError composable
  2. Handle the error itself
  3. Use clearError to move on once we’ve addressed the issue

This is how we’d use them together:

const error = useError();

const handleError = () => {
  clearError({
    redirect: '/dashboard',
  });
};

We can display info about the error, including the message and statusCode, and then use clearError to reset the error.

Here, we’ve only addressed 1 and 3, but how do we handle the actual error?

Well, that entirely depends on your application, and what was happening at the moment. If you were trying to log a user in, perhaps you need to show them a “Forgot Password” dialog. If they were trying to export a PDF report from a dashboard, maybe the right way to handle the error is to just retry the export.

However, in many cases, a good default is to redirect back to the main page. This is why clearError has the redirect option built-in:

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

The Two Error Types in Nuxt

Now, let’s take a moment to better understand how errors work in Nuxt 3.

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 Sentry or 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.

We’ve already seen how these global errors work, so let’s take a look at how client-side errors are different.

Client-Side Errors

The main difference with client-side errors is that they only exist in the user's browser.

The other difference is that they can be localized to a specific part of your application. Instead of crashing your entire app if something breaks, we can make sure that only a small part of your app breaks, leaving the rest fully functional.

We can do this using the NuxtErrorBoundary component that is built-in to Nuxt:

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

Any errors that happen in the component’s default slot are captured, preventing them from affecting the rest of the app. In this case, we’re wrapping the VideoPlayer component in a “protective shield” using NuxtErrorBoundary.

We can then use the error named slot to show a very specific error message that is tailored to this specific use-case. This is why I love NuxtErrorBoundary so much because it lets us create incredibly specific messages — and actions to recover from the error — instead of a generic, “whoops, something broke!”.

I wrote an entire deep dive on client-side error handling if you want to learn more.

Conclusion

We’ve seen how we can manage and work with errors in Nuxt 3, all the way from the backend to the frontend.

Using throw createError() we can trigger errors anywhere in our app — in event handlers, SSR, or client-side code.

Then, using the useError and clearError methods, we can gather info about the error and then handle it properly.

Even better, we can use NuxtErrorBoundary to handle client-side only errors without ever hitting the server. We can also provide extremely specific error messages and actions to provide an even better UX for our users.

If you want to learn more about error handling, check out Mastering Nuxt 3, where we show how to apply all of these techniques to a full-stack app. You can also check out the docs for more info.

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