This article explores preventing user frustrations in your Nuxt 3 application by building rock solid applications through error handling.
Get notified when we release new tutorials, lessons, and other expert Nuxt content.
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:
NuxtErrorBoundary
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!
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.
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:
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.
With Nuxt 3 we can pretty easily create our own custom error pages.
We have a few steps to do this:
error.vue
page is rendered.useError
to grab the error and inspect it.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.
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;
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.
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.
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:
abortNavigation()
— this will create a 404
and Page not found
error pageabortNavigation('Whooooops!')
— allows us to set a more descriptive error messageabortNavigation(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.
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.
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: