Better error handling in React and Axios

March 28, 2021

☕️ 7 minute read

Github: https://github.com/SeanningTatum/10-April-2021-Error-Handling

Slides: https://docs.google.com/presentation/d/1IT1d_hi2m1CiYVUwC5ubFM__Pych7nb_Cilhv8Hx7rU/edit?usp=sharing

Problem Statement

unknown error

Guess where this error came from? Upon first inspection - calling /api/items is the problem. But this raises a number of questions, such as

  • Which route called this? You might have inner routes.

  • Which file? My project is scaling and has over 100 possible files where that could've come from.

  • A 400 error can come in different shapes

    • Form validation error?

      • Which form value in particular? And what type of validation errors?
    • Problems with the Query Params?

    • etc.

better error

Now take a look at this error message.

  • Clear Error Type
  • Clear Error Message
  • Stack Trace is very clear, it tells you exactly which file and line the error was called
  • There's a source map link you can click which lets you view the code in the browser.

If you click the link you'd see something like this

source map error

Now that's more like it! Even people who are new the project can start debugging right away!

How do we add this?

Axios allows you to intercept requests and responses before they they are handled by .then and .catch. These are called interceptors (read more here).

If you've clicked the link you can create an instance and intercept the response like so.

// utils/axios.js

const instance = axios.create();

instance.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
}, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
});

export default instance

So far this is the default error handling that axios is already providing us, let's try meddling around with it to see how to play around with the interceptors.

instance.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
}, function (error) {
  
    if (error.code.status === 400) {
      return Promise.reject({
        message: "You've recieved an error!"
      })
    }
  
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
});

Let's see what that does in the console.

error object

Probably what you were expecting! It returned our custom object and even told us where the error was caught! Assuming you've wrapped it around a try/catch block or a .catch.

But this brings us to another problem - as we're trying to scale and maintain our systems, from a coding point this is still not 'simple enough'. We need to handle a lot of edge cases for errors, it's like our data structures and algorithms class where we have to consider every edge case possible and can easily end up like this.

function fetchData() {
  try {
    const data = await axios.get('/data')
    
    return data
  } catch (err) {
    if (err.code === '404') {
      setError('Object does not exist.')
      return
    }
    
    if (err.name === 'err001-auth') {
      history.push('/login')
      return
    }
    
    if (err.body.validation.length > 0) {
      const validations = err.body.validation.map(err => err.message)
      setValidations(validations)
      return
    }
  }
}

At the moment it's not hard to read or understand, but as errors get more complicated such as multiple permissions, handling errors from 3rd party apis and having different formats of errors it can get easily get out of hand if left unchecked. So what should we do? How can we make it readable in the code, abstract errors and easily debug?

function fetchData() {
  try {
    const data = await axios.get('/data')
    
    return data
  } catch (error) {
    if (error instanceof NotFoundError) {
      setError('Object does not exist.')
      return
    }
    
    if (error instanceof AuthorizationError) {
      // View what permissions were needed to access content
      console.log(error.permissions())
      
      history.push('/login')
      return
    }
    
    if (error instanceof ValidationError) {
      // Generate readable error message to display to user
      setError(error.generateErrorMessage())
      
      // Format Errors from Server
      setFormValueErrors(error.formatErrors())
      return
    }
    
    if (error instanceof ServerError) {
      history.push('/server-down')
      return
    }
  }
}

Now without even knowing anything about error codes and the different error conventions - I can read it what type of error it is in plain english and the error class has abstracted helper methods that I do not need to know about that are provided for me. Good for me 1 year later and any new developers that are joining the project!

Now let's see how we can catch custom error objects. First we're going to have to create one!

// utils/customErrors.js

export class BadRequestError extends Error {
  constructor(errors) {
    super('Something was wrong with your Request Body');
    this.name = 'BadRequestError';
    this.errors = errors || [];
  }

  // Assuming your data looks like this
  // {
  //   "errors": [
  //     {
  //       "location": "body",
  //       "msg": "Invalid value",
  //       "param": "username"
  //     },
  //     ...
  //   ]
  // }
  formatErrorsIntoReadableStr() {
    let str = 'There are formatting issues with';
    this.errors.forEach((error) => {
      str += `${error.param}, `;
    });

    return str;
  }
  
  // More custom code here if you want.
}

Now back into `utils/axios.js` let's throw our custom error instead of a simple object

// utils/axios.js

// NEW:- Import New Error Class
import {BadRequestError} from './errors'

instance.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
}, function (error) {
  
    if (error.code.status === 400) {
      // NEW:- Throw New Error
      throw new BadRequestError()
    }
  
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
});
function fetchData() {
  try {
    const data = await axios.get('/data')
    
    return data
  } catch (error) { 
    // Catch your new error here!
    if (error instanceof BadRequestError) {
      // Generate readable error message to display to user
      setError(error.formatErrorsIntoReadableStr())
      return
    }
    
  }
}

Now with this simple code and some object oriented programming that you were probably taught in college, frontend life has become easier.

Let's work together

If you need a Mobile App, Web App or Website.

Let's ideate, brainstorm and make magic happen.

Resume

For more details on my individual contributions, grab my resume!

Portfolio

A telltale of who i am, and screenshots of projects i've done

hello@seanurgel.dev

Drop me an email, I love my inbox

LinkedIn

Let's connect!