Normalizing API errors with axios.

Bonus - comes with types

Just take me to the main code snippet

Handling errors when fetching is a pain. Usually, I would just wrap the call in a try...catch block like this.

try {
    const res = await axios.get("http://localhost:4000/user")
    // do something 

} catch (error) {
   // handle error
}

Now, there's nothing wrong with this approach at all, however, what if I wanted to have types for my error? What if there were two or more API errors that I want to handle separately?

In this article, I'll show you how to normalize errors from axios to know what errors to expect.

*Code examples are written in typescript, but javascript developers will still be able to use this pattern.

Types of axios errors

There are 3 types of axios errors.

  1. The client received an error
    Example: (5xx,4xx)

  2. The client didn't receive a response
    Example: Network errors

  3. An error outside axios occurs.

Below is a snippet I use in most of my projects.

// apiHandler.ts
import { AxiosError } from "axios";

type ErrorMessage = {
  message: string;
  code: number;
};

type SuccessResponse<T> = {
  data: T;
  error: undefined;
};

type ErrorResponse = {
  data: undefined;
  error: ErrorMessage;
};

export const apiHandler = async <T>(
  promise: Promise<T>
): Promise<SuccessResponse<T> | ErrorResponse> => {
  return promise
    .then((data) => ({ data: data, error: undefined }))
    .catch((error: AxiosError) => {
      if (error.response) {
        // The client received an error ex. (5xx, 5xx)
        return {
          data: undefined,
          error: error.response?.data,
        };
      } else {
        // The client didn't receive a response ex. Network Error
        // OR an error outside axios happens
        return {
          data: undefined,
          error: {
            // The code could be anything you want
            // Just make sure it is understood that code is
            // meant for network errors
            code: 0,
            message: error.message,
          },
        };
      }
    });
};

And I use it like this

// sampleCall.ts

interface User {
  name: string;
  email: string;
}

const getUser = async () => {
  const axiosRes = await axios.get<User>("http://localhost:4000/user");
  return axiosRes.data;
};

const doSomethingWithUser = async () => {
    const {data, error} = await apiHandler(getUser())

    if(error){
        console.log(error.code)
        console.log(error.message)
        // Fail gracefully
    }
    // No errors now do something with that data


}

Another advantage of this is that you could handle errors from separate calls differently using object destructuring.

Like this

// twoSampleCalls.ts


// ...Irrelevant code

const doSomethingWithUserAndItem = async () => {
  const { data: userData, error: userError } = await apiHandler(getUser());
  const { data: itemData, error: itemError } = await apiHandler(getUser());

  if (userError) {
    // Handle error from user call
  }

  if (itemError) {
    // Handle error from item call
  }

  // Do something with data
};

I prefer this syntax by far over try...catch and .then() and .catch(). How about you?