Skip to main content

By Rachel Opperman

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:

  1. Destructuring the Axios error response in a Vuex action
  2. 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:

  1. There's no guarantee that there will be a response to destructure from the Axios error.
  2. 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 the plugins directory and register the plugin in the Nuxt config
  • Add an onError handler
  • Add an if block to the onError handler that checks for error.response and performs some logic if error.response is undefined

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.

By Rachel Opperman

Engineer

What happens when you cross years of study in biology and medicine with a degree in computer science? You get someone like Rachel.