DEV Community

Cover image for A Simple Guide to Asynchronous JavaScript: Callbacks, Promises & async/await
Mangabo Kolawole
Mangabo Kolawole

Posted on • Updated on

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

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:

  • Asynchronous Callbacks
  • Promises
  • Async / Await

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);
Enter fullscreen mode Exit fullscreen mode

You'll have an output in this order.

Hello World.
Greetings came first.
11
Enter fullscreen mode Exit fullscreen mode

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)

  • Jack: Hi. Please can I have a coffee? (First asynchronous task)
  • First Attendant: For sure. Do you want something else?
  • Jack: A piece of cake while waiting for the coffee to be ready. (Second asynchronous task)
  • First Attendant: For sure. ( Launch the preparation of the coffee )
  • First Attendant: Anything else?
  • Jack: No.
  • First Attendant: 5 dollars, please.
  • Jack: Pay the money and take a seat.
  • First Attendant: Start serving the next customer.
  • Jack: Start checking Twitter while waiting.
  • Second Attendant: Here's your cake. (Second asynchronous task call returns)
  • Jack: Thanks
  • First attendant: Here's your coffee. (First asynchronous task call returns)
  • Jack: Hey, thanks! Take his stuff and leave.

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 :

  • Asynchronous callbacks
  • Promises
  • And the async/await syntax.

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");
Enter fullscreen mode Exit fullscreen mode

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 :

  • array.map(callback)
  • array.forEach(callback)
  • array.filter(callback)
let fruits = ['orange', 'lemon', 'banana']

fruits.forEach(function logFruit(fruit){
    console.log(fruit);
});
Enter fullscreen mode Exit fullscreen mode

Output

orange
lemon
banana
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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 :

  • pending: the initial state of the promise
  • fulfilled: the operation is a success
  • rejected: the operation is a failure
  • settled: the operation is either fulfilled or settled, but not pending anymore.

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

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

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.

  • Jack's Mom: Hey Jack! Can you go to the store and get some milk? I need more to finish the cake.
  • Jack: For sure, Mom!
  • Jack's Mom: While you are doing that, I'll be dressing the tools to make the cake. (Async task) In the meantime, let me know if find it. (success callback)
  • Jack: Great! But what if I don't find the milk?
  • Jack's Mom: Then, get some chocolate instead. (Failure callback)

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");
    }
});
Enter fullscreen mode Exit fullscreen mode

Then, this promise can be used like this:

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

Here :

  • then(): takes a callback for success case and executes when the promise is resolved.
  • catch(): takes a callback, for failure and executes if the promise is rejected.
  • finally(): takes a callback and always returns when the premise is settled. It's pretty useful when you want to perform some cleanups.

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();
    })
};
Enter fullscreen mode Exit fullscreen mode

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."))
Enter fullscreen mode Exit fullscreen mode

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

Output

Output

Rules for writing promises

  • You can't call both resolve or reject in your code. As soon as one of the two functions gets called, the promise stops and a result is returned.
  • If you don’t call any of the two functions, the promise will hang.
  • You can only pass one parameter to resolve or reject. If you have more things to pass, wrap everything in an object.

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 :

  • call his Mom;
  • then buy more milk;
  • then buy chocolates;
  • and the list goes on.
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.");
    });
Enter fullscreen mode Exit fullscreen mode

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.

  • Friends: When are you ready? We'll pick you.
  • Jack: In 20 minutes. I promise.

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);
    })
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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() {
    ...
}
Enter fullscreen mode Exit fullscreen mode

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();
    ...
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode

And that's it for async/await.

This syntax has simple rules:

  • If the function you are creating handles async tasks, mark this function using the async keyword.

  • await keyword pauses the function execution until the promise is settled (fulfilled or rejected).

  • An asynchronous function always returns a Promise.

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;
};
Enter fullscreen mode Exit fullscreen mode

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."));
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (3)

Collapse
faheem_khan_dev profile image
Faheem Khan

Nice Explanation 🔥
Very easy to follow. Keep up the good work 🙂

Collapse
briannaxrene profile image
briannaxrene

Great read, but I think these two states need switching?
[settled]: the operation is a failure
[rejected]: the operation is either fulfilled or settled, but not pending anymore.

Collapse
koladev profile image
Mangabo Kolawole Author

Oh yes! Definitely, thanks for this correction.