A Simple Guide to Asynchronous JavaScript: Callbacks, Promises & async/await

Posted on Aug 12, 2021

Asynchronous programming in JavaScript is one of the fundamental concepts to grasp to write better JavaScript.

Today, we’ll learn about asynchronous JavaScript, with some real-world examples and some practical examples as well. Along with this article, you’ll understand the functioning of:

Table of content

1 - Synchronous vs Asynchronous

Before going into asynchronous programming, let’s talk about synchronous programming first.

For example,

let greetings = "Hello World.";

let sum = 1 + 10;

console.log(greetings);

console.log("Greetings came first.")

console.log(sum);

You’ll have an output in this order.

Hello World.
Greetings came first.
11

That’s synchronous. Notice that while each operation happens, nothing else can happen.

JavaScript is monothread at its core: when one block of code is being executed, no other block of code will be executed.

Asynchronous programming is different. To make it simple, when JavaScript identifies asynchronous tasks, it’ll simply continue the execution of the code, while waiting for these asynchronous tasks to be completed.

Asynchronous programming is often related to parallelization, the art of performing independent tasks in parallel.

How is it even possible?

Trust me, we do things in an asynchronous way without even realizing it.

Let’s take a real-life example to better understand.

Real life example: Coffee shop

Jack goes to the coffee shop and goes directly to the first attendant. (Main thread)

To make it simple, while you are waiting for someone else to do stuff for you, you can do other tasks or ask others to do more stuff for you.

Now that you have a clear idea about how asynchronous programming works, let’s see how we can write asynchronous with :

2 - Asynchronous Callbacks: I’ll call back once I’m done!

A callback is a function passed as an argument when calling a function (high-order function) that will start executing a task in the background. And when this background task is done running, it calls the callback function to let you know about the changes.

function callBackTech(callback, tech) {
    console.log("Calling callBackTech!");
    if (callback) {
        callback(tech);
    }
    console.log("Calling callBackTech finished!");
}

function logTechDetails(tech) {
    if (tech) {
        console.log("The technology used is: " + tech);
    }
}

callBackTech(logTechDetails, "HTML5");

Output

Output Callback

As you can see here, the code is executed each line after each line: this is an example of synchronously executing a callback function.

And if you code regularly in JavaScript, you may have been using callbacks without even realizing it. For example :

let fruits = ['orange', 'lemon', 'banana']

fruits.forEach(function logFruit(fruit){
    console.log(fruit);
});

Output

orange
lemon
banana

But callbacks can also be executed asynchronously, which simply means that the callback is executed at a later time than the higher-order function.

Let’s rewrite our example using setTimeout() function to register a callback to be called asynchronously.

function callBackTech(callback, tech) {
    console.log("Calling callBackTech!");
    if (callback) {
        setTimeout(() => callback(tech), 2000)
    }
    console.log("Calling callBackTech finished!");
}

function logTechDetails(tech) {
    if (tech) {
        console.log("The technology used is: " + tech);
    }
}

callBackTech(logTechDetails, "HTML5");

Output

Output Async callbacks

In this asynchronous version, notice that the output of logTechDetails() is printed in the last position.

This is because the asynchronous execution of this callback delayed its execution from 2 seconds to the point where the currently executing task is done.

Callbacks are old-fashioned ways of writing asynchronous JavaScript because as soon you have to handle multiple asynchronous operations, the callbacks nest into each other ending in callback hell.

Callback Hell

To avoid this pattern happening, we will see now Promises.

3 - Promise: I promise a result!

Promises are used to handle asynchronous operations in JavaScript and they simply represent the fulfillment or the failure of an asynchronous operation.

Thus, Promises have four states :

This is the general syntax to create a Promise in JavaScript.

let promise = new Promise(function(resolve, reject) {
    ... code
});

resolve and reject are functions executed respectively when the operation is a success and when the operation is a failure.

To better understand how Promises work, let’s take an example.

This analogy isn’t terribly accurate, but let’s go with it.

Here’s what the promise will look like, supposing that Jack has found some milk.

let milkPromise = new Promise(function (resolve, reject) {

    let milkIsFound = true;

    if (milkIsFound) {
        resolve("Milk is found");
    } else {
        reject("Milk is not found");
    }
});

Then, this promise can be used like this:

milkPromise.then(result => {
    console.log(result);
}).catch(error => {
    console.log(error);
}).finally(() => {
    console.log("Promised settled.");
});

Here :

Let’s use a real-world example now, by creating a promise to fetch some data.

let retrieveData = url => {

    return new Promise( function(resolve, reject) {

        let request = new XMLHttpRequest();
        request.open('GET', url);
    
        request.onload = function() {
          if (request.status === 200) {
            resolve(request.response);
          } else {
            reject("An error occured!");
          }
        };
        request.send();
    })
};

The XMLHttpRequest object can be used to make HTTP request in JavaScript.

Let’s use the retrieveData to make a request from https://swapi.dev, the Star Wars API.

const apiURL = "https://swapi.dev/api/people/1";

retrieveData(apiURL)
.then( res => console.log(res))
.catch( err => console.log(err))
.finally(() => console.log("Done."))

Here’s what’s the output will look like.

Output

Output

Rules for writing promises

4 - async/await: I’ll execute when I am ready!

The async/await syntax has been introduced with ES2017, to help write better asynchronous code with promises.

Then, what’s wrong with promises? The fact that you can chain then() as many as you want makes Promises a little bit verbose. For the example of Jack buying some milk, he can :

milkPromise.then(result => {
        console.log(result);
    }).then(result => {
        console.log("Calling his Mom")
    }).then(result => {
        console.log("Buying some chocolate")
    }).then(() => {
        ...
    })
    .catch(error => {
        console.log(error);
    }).finally(() => {
        console.log("Promised settled.");
    });

Let’s see how we can use async/await to write better asynchronous code in JavaScript.

The friend party example

Jack is invited by his friends to a party.

Well, actually Jack will be ready in 30 minutes. And by the way, his friends can’t go to the party without him, so they’ll have to wait.

In a synchronous way, things will look like this.

let ready = () => {

    return new Promise(resolve => {

        setTimeout(() => resolve("I am ready."), 3000);
    })
}

The setTimeout() method takes a function as an argument (a callback) and calls it after a specified number of milliseconds.

Let’s use this Promise in a regular function and see the output.


function pickJack() {

    const jackStatus = ready();
    
    console.log(`Jack has been picked: ${jackStatus}`);

    return jackStatus;
}

pickJack(); // => Jack has been picked: [object Promise]

Why this result? The Promise hasn’t been well handled by the function pickJack. It considers jackStatus like a regular object.

It’s time now to tell our function how to handle this using the async and await keywords.

First of all, place async keyword in front of the function pickJack().

async function pickJack() {
    ...
}

By using the async keyword used before a function, JavaScript understands that this function will return a Promise. Even, if we don’t explicitly return a Promise, JavaScript will automatically wrap the returned object in a Promise.

And next step, add the await keyword in the body of the function.

    ...
    const jackStatus = await ready();
    ...

await makes JavaScript wait until the Promise is settled and returns a result.

Here’s how the function will finally look.

async function pickJack() {

    const jackStatus = await ready();
    
    console.log(`Jack has been picked: ${jackStatus}`);

    return jackStatus;
}

pickJack(); // => "Jack has been picked: I am ready."

And that’s it for async/await.

This syntax has simple rules:

Here’s a practical example using async/await and the fetch() method. fetch() allows you to make network requests similar to XMLHttpRequest but the big difference here is that the Fetch API uses Promises.

This will help us make the data fetching from https://swapi.dev cleaner and simple.

async function retrieveData(url) {
    const response = await fetch(url);

    if (!response.ok) {
        throw new Error('Error while fetching resources.');
    }

    const data = await response.json()

    return data;
};

const response = await fetch(url); will pause the function execution until the request is completed. Now why await response.json()? You may be asking yourself.

After an initial fetch() call, only the headers have been read. And as the body data is to be read from an incoming stream first before being parsed as JSON.

And since reading from a TCP stream (making a request) is asynchronous, the .json() operations end up asynchronous.

Then let’s execute the code in the browser.

retrieveData(apiURL)
.then( res => console.log(res))
.catch( err => console.log(err))
.finally(() => console.log("Done."));

And that’s all for async/await

Conclusion

In this article, we learned about callbacks, async/await and Promise in JavaScript to write asynchronous code. If you want to learn more about these concepts, check these amazing resources.

Share Tweet