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.
The client received an error
Example: (5xx,4xx)The client didn't receive a response
Example: Network errorsAn 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?