Fetch API, Async/Await in a few bites

Alice Moretti
Nerd For Tech
Published in
6 min readApr 12, 2021

--

The Fetch API

Fetch allows to perform network requests by writing way less code than using XMLHttpRequest, and it implements the promise API under the hood.

An example of code using fetch:

fetch("./data/list.json")
.then((response) => {
console.log("resolved", response);
})
.catch((err) => {
console.log("error retrieving data", err);
});
  1. Fetch takes as an argument the URL endpoint where to grab the data from. This can be either an internal or external resource.
  2. Fetch returns a promise which can be tackled with the .then method we have seen in the previous blog post.
  3. If the promise is resolved, the callback function of .then is fired. In case the promise is rejected (for example there is a problem in retrieving the data), the callback function of .catch is fired.

Errors with the URL endpoint

An interesting thing to underline is that in case we misspelled the URL endpoint, the promise would still be resolved but the response object statusText property would be empty.

statusText property on the response object

To make sure we get an object containing the actual data, we can check the request status value and, in case this differs from 200, highlight an error (below we’ll see how to throw an error so that the error object is catched).

fetch("./data/listsssss.json")
.then((response) => {
if (response.status !== 200) {
console.log("there is an error");
}
console.log("resolved", response);
})
.catch((err) => {
console.log("error retrieving data", err);
});

Where are my data?

Let’s say the endpoint is spelled correctly and we get the data object back, if you console.log the response you will notice that the actual data are not found there. To be able to see and use this data we need to apply the .json method which parses the string of data and returns a js object. This method is part of the response object we get back by using the fetch API and returns a promise. This means that we cannot store its value inside a variable like follows:

let myData = response.json() THIS IS WROOOOOONG!

Instead, we can return the promise and then chain another .then method which will take the actual parsed data as the argument:

fetch("./data/list.json")
.then((response) => {
console.log("resolved", response);
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log("error retrieving data", err);
});

One thing which might be super simple but myself I didn’t want to take for granted: both “response” and “data” are arbitrary words. Yes, I changed “response” to “MarioRossi” to be fancy and the code worked (then I thought that maybe reverting the text would have been wise, especially as I understand there are some conventions to make the code more readable and understandable :-) ).

The fetch API in three steps

To wrap it up, when we are using fetch:

  1. we fetch the data by providing the URL endpoint
  2. we return the response.json promise
  3. we chain a .then method and finally have access to the data

Above all, the code is much simpler than writing an XMLHttpRequest!

async/await

async and await syntax allows us to chain promises more easily and cleanly. To make an asynchronous function with async we write:

let myAsyncFunction = async () => {
...here we write all the asynchronous code...
}

By adding the “async” keyword, the entire function returns a promise. We can prove this by putting the function inside a variable and then console.log it:

let newVariable = myAsyncFunction()
console.log(newVariable)
promise

async/await works well with fetch as it allows to handle the promises in a super-easy way. Let’s have a look:

Fetch returns a promise and therefore we cannot store the result object inside a variable. As a solution to this, async comes with a useful keyword “await”:

let myAsyncFunction = async () => {
let response = await fetch("./data/list.json");
console.log(response);
};

By adding the “await” keyword, the response object is not stored inside the response variable until the promise is resolved.

Moving on, by console.log the response, we will see that the actual data is not visible and, as above, we need to apply the .json method which returns a promise. Again we can tackle the promise by using the “await” keyword:

let myAsyncFunction = async () => {
let response = await fetch("./data/list.json");
let data = await response.json;
console.log(data);
return data;
};

Calling myAsyncFunction() returns a promise and, since the code inside the function returns the data, we can apply the .then method which will take the data as argument (we describe what happens in case of an error in the next paraghraph):

myAsyncFunction().then((data) => {
console.log("resolved", data);
});

As it is an asynchronous function, myAsyncFunction() doesn’t prevent the rest of the code from running and we can prove it with the following code:

console.log("1");
console.log("2");
console.log("3");
myAsyncFunction()
.then((data) => {
console.log("resolved", data);
})
.catch((err) => {
console.log("rejected", err);
});
console.log("4");

In the console, we will see 1,2,3,4 and then, last, the response object.

The async/await syntax not only allows us to chain promises more cleanly with the await keyword, but also allows us to store an asynchronous function inside a variable which makes it easier for the function to be called.

Throwing errors

In case there is a problem with the syntax of the json file (let’s say we forgot to put a double quote around a key), then the promise returned by response.json would be rejected and also the promise of the overall async function would be. That means we could catch the error. In the example below, we access the error property on the err object:

.catch((err) => {
console.log("rejected", err.message);
});
what we see in the console.

The other error scenario I wanna talk about is slightly more difficult to handle. In case the ULR endpoint was misspelled, the promise would still be resolved, the .json method would still parse the response and the result would be an error.

To prevent this from happening, we need to manually check the status of the response and, in case this is different from 200, we throw an error. This will prevent the .json method from “kicking in”.

let myAsyncFunction = async () => {
const response = await fetch("./data/lists.json");
if (response.status !== 200) {
throw new Error("cannot fetch data");
}
let data = await response.json();
return data;
};
myAsyncFunction()
.then((data) => {
console.log("resolved", data);
})
.catch((err) => {
console.log("rejected", err.message);
});

By writing “throw new Error” we are creating a new error object. Whenever we throw an error inside an asynchronous function, then the promise returned by it is rejected and thus, we can catch the error.

The error message will now be “cannot fetch data”.

I hope this blog post and the previous one have helped to understand a bit more the topic of networking requests.

Please leave a comment below if you believe some parts of the article could be improved or you have any thoughts you would like to share on the topic.

--

--

Alice Moretti
Nerd For Tech

Frontend-developer, nature addicted, runner, coffee & yoga lover living in Helsinki.