I'm part of a group that holds introductory courses on web development for students and new employees in Bekk. In the course we say that there are three ways to retrieve data in JavaScript, but we only explain one of the methods. Now, let's see how each of them works: callbacks, promises and async / await.
In the spirit of Christmas, we'll use a suitable analogy. Let's say Santa requests that an elf fetches a gift for a child on his list. He does not know when the elf will be back, but when it does he wants to put the gift into his bag of toys. This is 2020, after all, so of course he uses JavaScript to complete the task :santa:
Our elf in this analogy will be represented by the function getGiftForChild()
, which we will define for each of the three methods. The elf will retrieve the gifts using the magical url https://santas-gift-storage.northpole/gifts/nameOfChild
, which returns a gift for a given child's name. We also have access to the global array bagOfToys
, which is to be filled up with Christmas presents before Santa is on his way.
const bagOfToys = [];
const getGiftForChild = (name) => {
const url = `https://santas-gift-storage.northpole/gifts/${name}`;
// Fetch gift
};
Callbacks
The first way to fetch data utilises that we can pass a reference to a function A
as an argument to another function B
, which then is going to call A
at some point. Here, A
is called the callback-function, while B
is the higher order-function. This is a very common pattern in JavaScript, so it's likely you've seen it before – in event handlers, timeouts or even in the map, filter and reduce list-functions.
There are multiple ways to fetch data, but we're going to look at the classic, well-used API XMLHttpRequest. If you're not familiar with it, don't worry – the details aren't too important. We're interested in the way we use our callback, and I'll explain it below.
First, let's define the main function fetchChristmasGift()
, which Santa can use to send an elf on his way with a child's name.
const fetchChristmasGift = (name) => {
getGiftForChild(name);
};
const getGiftForChild = (name) => {
const url = `https://santas-gift-storage.northpole/gifts/${name}`;
// Fetch gift
};
Then, we implement the gift retrieval using an XMLHttpRequest
:
const getGiftForChild = (name) => {
const url = `https://santas-gift-storage.northpole/gifts/${name}`;
const request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState === 4) {
// Callback time!
}
};
request.open("GET", url);
request.send();
};
The event onreadystatechange
is triggered when the state of the request changes. We assign a function to this event, which will be called when it occurs. If the state has changed to 4 = DONE
, we want to do something: we want to call a callback function!
So let's create a callback function, to decide what happens when we have fetched our gift. Our instructions were to add it to Santa's bag of gifts, so we'll do that. While we're at it, let's include a check to see if we actually got the gift:
const callbackFunction = (request) => {
if (request.status == 200) {
const gift = request.response;
bagOfToys.push(gift);
} else {
console.log("Oops! No gift for this child");
}
};
In order for the elf to know what to do with the gift, he must be sent some instructions. Therefore, we must pass the callback function to the higher order function getGiftForChild()
. Let's take a look at the final result:
const bagOfToys = [];
const fetchChristmasGift = (name) => {
getGiftForChild(name, callbackFunction);
};
const getGiftForChild = (name, callback) => {
const url = `https://santas-gift-storage.northpole/gifts/${name}`;
const request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState === 4) {
callback(request);
}
};
request.open("GET", url);
request.send();
};
const callbackFunction = (request) => {
if (request.status == 200) {
const gift = request.response;
bagOfToys.push(gift);
} else {
console.log("Oops! No gift for this child");
}
};
And that's it. We used a callback to do something with our data after we successfully fetched it.
But callbacks can easily get messy. There is a reason why there is something called callback hell :fire: (just Google it and see), and why this is not the preferred way of doing things. Let's move on to a new type of object that has another take on the matter.
Promises
The second way to fetch data uses Promises to handle asynchronous calls. Promises are objects that make asynchronicity a bit more tidy. For this purpose we use a function called fetch()
from the Fetch API. Unlike the previous method, the responses are wrapped in Promises, so you don't have to explicitly handle all the nitty-gritty details anymore!
We start by defining the elf function getGiftForChild()
which takes a child's name as input and fetches the gift from our URL using fetch()
. This function will return a response in the form of a Promise
.
const getGiftForChild = (name) =>
fetch(`https://santas-gift-storage.northpole/gifts/${name}`);
Then, we can use the Promise prototype methods then()
and catch()
to do something when the Promise either resolves :white_check_mark: or is rejected :x:. Let's define the function fetchChristmasGift()
like before, and call getGiftForChild()
to fetch our gift:
const fetchChristmasGift = (name) => {
const promise = getGiftForChild(name);
promise
.then((gift) => bagOfToys.push(gift))
.catch((error) => console.log("Oops! No gift for this child"));
};
In fact, this is also a way of using callbacks. We define what is to happen when we get a response, passing in a function to both then()
and catch()
. With Promises it's just being handled in a smoother way than our first example. But can it get even better? Keep on reading!
Async / Await
The third way to fetch data builds on Promises, but is generally a more elegant way of handling them that looks a lot more like synchronous programming. Assume we still have our function getGiftForChild()
which returns a Promise:
const getGiftForChild = (name) =>
fetch(`https://santas-gift-storage.northpole/gifts/${name}`);
This request can either fetch a gift successfully, or fail trying. Instead of using then()
and catch()
to handle these two cases, we can define an asynchronous function.
Here, we make use of the two magical keywords async
and await
, to explicitly wait for the Promise to resolve before we move on, and say that we do so.
In our analogy, it would look something like this:
const fetchChristmasGift = async (name) => {
const gift = await getGiftForChild(name);
bagOfToys.push(gift);
};
We wait until we've retrieved a gift successfully, and then we'll add it to the bag :gift: To be sure, we can wrap it in a try / catch
block to handle if the Promise is rejected:
const fetchChristmasGift = async (name) => {
try {
const gift = await getGiftForChild(name);
bagOfToys.push(gift);
} catch {
console.log("Oops! No gift for this child");
}
};
We have to declare fetchChristmasGift()
asynchronous with async
, since it's using await
within. That way, it's clear that something is being awaited where the async function fetchChristmasGift()
is called. We can decide if we want to await that function as well, or go on with our business as usual in parallel. After all, the North Pole is a busy place.
And that's about it! I hope you learned something new about fetching data from this quick introduction, whether you're new to JavaScript or a veteran who has long since forgotten why you write code the way you're used to. Hopefully, Santa read this too and was able to find your gift in time for Christmas Eve :christmas_tree: