Consolidating Error Handling in Nuxt.js Apps
If you've ever worked with Nuxt, you know that errors can be handled in a variety of ways, particularly when those errors result from HTTP requests made with Axios.
There are 3 typical places in a Nuxt app where error handling logic can be written — the Vuex action, the page middleware, and the component.
While it's acceptable to use all 3 locations, wouldn't it be nice to have a standardized way of dealing with the errors that are received, regardless of their structure? That's the crux of the problem — your error handler has to be flexible enough to deal with different responses so that it doesn't cause errors of its own.
In this post, we'll discuss how we previously handled Axios errors in our Nuxt apps, why this approach was problematic, and how we fixed it by creating a package for consolidated error handling. We'll then run through some examples of using the new approach.
The old approach
Our old error handling approach involved doing the following 2 things:
- Destructuring the Axios error response in a Vuex action
- Using the destructured response in a component
Let's look at some example code.
Destructuring the error response
// Vuex action
async someAction() {
try {
const response = await this.$axios.$get('/some/endpoint')
// Do something with the response (e.g., call a mutation on it)
} catch ({ response }) {
throw response
}
}
In this action, we're destructuring response
from the Axios error so that we can use it directly in the component, as opposed to having to use error.response
.
Using the destructured response in a component
// A method in a component
async handleSubmit() {
try {
await this.$store.dispatch('someAction')
} catch (errors) {
// Set validation errors on a form
this.form.setErrors(errors.data.errors)
}
}
errors
is equal to the response
destructured from the Axios error, since that's what we're throwing in the Vuex action above. If you've written something similar to this, then you can probably see what the potential problems are with this approach. Let's move on and discuss those.
The problem with the old approach
There are 2 main problems with the error handling approach taken in the previous section:
- There's no guarantee that there will be a response to destructure from the Axios error.
- We were expecting to have
errors.data.errors
in the component, but the error object that we get may not have that structure.
Having a response to destructure isn't guaranteed
We can't assume that we'll have response
on the error that we get back, since that may not always be the case. For example, if a network error occurs during an HTTP request, then error.response
will be undefined
. If that's the case, the Vuex action will be throwing undefined
, not an object, and our component will be trying to access properties on an object that isn't defined. Our catch
block will then result in an error of its own.
The response structure may be different from what was expected
We also can't assume that the error response will have a specific structure. In the example above, we're assuming that the error was a 422
validation error, which would have a structure like this:
response: {
data: {
message: 'The given data was invalid.',
errors: {
name: ['The name field is required.'],
},
},
}
Behind the scenes, our this.form.setErrors()
method would perform some specific logic on the properties of response.data.errors
. However, something else could go wrong, and we could get a 404
, a 500
, or some other error instead, which would not necessarily have response.data.errors
. If this were the case, then our component method's catch
block would fail because response.data.errors
would be undefined
.
So how can we fix these issues? The purpose of our error handlers is what the name implies — to handle errors. We don't want to potentially create other errors in trying to handle the ones we already have. Luckily, there's a solution, and it's simpler than you might think.
Fixing the problem
In order to fix the issues discussed above, we have to do the following:
- Handle network errors at the highest level possible
- Create a class that will generate specific error messages for specific response statuses (e.g.,
401
,404
) - Create a class that will handle any error response
Using an Axios interceptor to handle network errors
It's important to handle network errors at the highest level possible so that the error handlers within components don't have to handle them. This is where an Axios interceptor comes in. You can read more about Axios interceptors in the docs, but here's the gist of what we have to do:
- Make an
axios.js
file in theplugins
directory and register the plugin in the Nuxt config - Add an
onError
handler - Add an
if
block to theonError
handler that checks forerror.response
and performs some logic iferror.response
isundefined
Let's look at some example code.
// nuxt.config.js
plugins: [
{ src: '@/plugins/axios.js' }
]
// plugins/axios.js
export default ({ $axios, redirect, app }, inject) => {
$axios.onError((error) => {
if (error.response === undefined) {
// Display a flash notification
app.notify({
title: 'Network Error: Please refresh and try again.',
type: 'error',
duration: -1,
})
throw error
}
// Handle other types of errors (e.g., redirect to login on 401 errors)
throw error
})
}
In this example, each time an Axios error occurs, the error is intercepted and checked to see if it has a response
property. If it doesn't, this indicates that a network error occurred. If that's the case, we're displaying a flash notification to the user and requesting that they refresh their browser.
Creating a class to generate error messages
A crucial part of good frontend error handling is displaying a message that is not only useful to the user, but that will also provide some helpful information to the support team and developers. This is where an ErrorMessages
class comes in. This class will be used in conjunction with an Errors
class, which is what will be responsible for parsing errors and returning the error message.
Let's take a look at the ErrorMessages
class.
// ErrorMessages.js
export const defaultErrorMessages = {
401: `Not Authenticated: Sorry, you have to be logged in to access this!`,
403: `Not Authorized: Sorry, you can't access this!`,
404: `Not Found: We couldn't find what you're looking for. Please refresh and try again, or contact the support team.`,
422: 'Validation Error',
500: 'Server Error: Please contact the support team.',
}
export const defaultGeneralErrorMessage = 'Error: Please refresh and try again, or contact the support team.'
export default class ErrorMessages {
/**
* @param {object | null} errorMessages
* @param {string | null} generalErrorMessage
*/
constructor(errorMessages, generalErrorMessage) {
this.errorMessages = errorMessages || defaultErrorMessages
this.generalErrorMessage = generalErrorMessage || defaultGeneralErrorMessage
}
/**
* Retrieve the error message for a particular status code.
*
* @param {number} errorStatus
* @return {string} errorMessage
*/
getErrorMessage(errorStatus) {
if (errorStatus && this.errorMessages[errorStatus]) {
return this.errorMessages[errorStatus]
}
return this.generalErrorMessage
}
}
In this file, we create some default error messages to use in case custom messages aren't provided. We then simply use the getErrorMessage
method to get the error message for the particular error status (e.g., 401
), if there's a status that matches. If not, we return the general error message. The getErrorMessage
method will be used by the Errors
class, which we'll look at next.
Creating a class to handle any error response
The purpose of the Errors
class is to receive Axios errors and parse them, regardless of their structure, so that components can have a consistent error response to use when handling errors. We'll only touch on a couple of the most relevant class methods here, but you can view the full class on GitHub if you're interested.
First, we need to initialize the class in the constructor. This is where the ErrorMessages
class is used.
/**
* @param {object | null} customMessages
* @param {string | null} customDefaultMessage
*/
constructor(customMessages = null, customDefaultMessage = '') {
this.errors = {}
this.errorMessages = new ErrorMessages(customMessages, customDefaultMessage)
}
The first thing we'll do when we use an instance of this class is set all errors, regardless of the error response structure.
/**
* Set all errors, regardless of type.
*
* @param {*} errors
* @return {self}
*/
setAll(errors) {
this.errors = errors
return this
}
setAll
returns this
so that it's possible to chain other methods onto it. One such method is parse
, which will parse the errors and return a consistent payload based upon the error status.
/**
* Parse errors and return a payload based on error status.
*
* @return {object} errorResponse
*/
parse() {
const status = this.errors.response ? this.errors.response.status : this.errors.status
const errors = this.errors.response ? this.errors.response.data : this.errors.data
if (status === 422) {
this.setValidation(errors)
}
return {
status,
message: this.errorMessages.getErrorMessage(status),
}
}
The setValidation
method will destructure errors
, which is the object that will contain validation errors, from the error response and set those as the class instance's errors
. This makes it easier to work with validation errors.
Now let's take a look at how we can actually use this new approach.
Using the new approach
We've turned this approach into a package, @zaengle/error-handler
, which you can view on npm. You can also view the package code and documentation on GitHub. In Nuxt, third-party packages are commonly used as plugins, so we’ll walk through how to make one for the error handler package. We’ll then examine how to use that plugin at both the page level, within anonymous middleware, and at the component level, within a custom method.
Creating a Nuxt plugin
Creating a Nuxt plugin will prevent us from having to import the Error
class in every component in which we need to use it.
// plugins/error-handler.js
import ErrorHandler from '@zaengle/error-handler'
export default (context, inject) => {
inject('errorHandler', new ErrorHandler())
}
// nuxt.config.js
plugins: [
{ src: '@/plugins/error-handler.js' }
]
Using the plugin in a page
It's common in a Nuxt app to use the middleware
function to request data on page load. The potential errors from such requests can be handled in the following manner.
// pages/somePage.vue
async middleware({ store, error, $errorHandler }) {
try {
// This could also be an Axios request
await store.dispatch('someAction')
} catch (errors) {
const errorResponse = $errorHandler.setAndParse(errors)
error({
statusCode: errorResponse.status,
message: errorResponse.message
})
}
}
Since we added the error handler to the app's context
in the plugin, it can be destructured as the $errorHandler
argument in the middleware
function.
Notice that we do not do any destructuring of the errors
argument passed to the catch
block — this is to avoid one of the issues we discussed above, which is destructuring something that may not be present. Instead, we simply pass whatever errors we receive to the error handler's setAndParse
method, which is a method that simply calls the setAll
and parse
methods that we saw in the previous section. We then pass the status
and the message
, which we know we'll be getting because they're from an object of our own construction, to Nuxt's error
method so that the error message can be displayed on the error page.
Using the plugin in a component
There are also instances in which we'll need to handle errors in the methods in our components. Let's take a look at an example.
// components/someComponent.vue
export default {
methods: {
handleSomething() {
try {
// This could also be an Axios request
await this.$store.dispatch('someAction')
} catch (errors) {
const errorResponse = this.$errorHandler.setAndParse(errors)
// Do something with errorResponse (e.g., display a flash notification)
this.notify({
status: errorResponse.status,
message: errorResponse.message
})
}
}
}
}
As we saw in the page example, we do not do any destructuring of the errors
argument passed to the catch
block. Instead, we simply pass whatever errors we get to our error handler and let the class take care of parsing them.
Wrapping up
Error handling is one of the most important aspects of a frontend JavaScript application, but it can also be something that's difficult to do gracefully. This is largely due to the fact that we may not always get the error structure that we're expecting, or we may not even get an error response at all. To circumvent these potential issues, we can do the following:
- Deal with network errors at the highest level (i.e., in an Axios interceptor) so that our components aren't trying to handle errors that don't have a response
- Avoid destructuring error arguments in components and instead pass whatever errors we get to a class that will parse them and give us a consistent, predictable response that can be used in our error handlers
If you'd like to implement this approach in your apps, feel free to give our package a try. If you have alternate methods of dealing with the error handling issues mentioned in this post, please let us know @zaengle.
Want to read more tips and insights on working with a software development team that wants to help your organization grow for good? Sign up for our bimonthly newsletter.
Engineer
What happens when you cross years of study in biology and medicine with a degree in computer science? You get someone like Rachel.