CodeTechSphere

Asynchronous programming in Javascript

Cover Image for Asynchronous programming in Javascript
Muhammad Khawaja
Muhammad Khawaja

While learning asynchronous programming in Javascript for web development/Node.js, I found that there were a lot of shortcomings in my understanding of how to maximize the benefits of asynchronous programming in Javascript runtime environments, especially in web development. I was throwing callbacks/promises here and there and messing up execution/logic in the DOM. In this article, I will break down the fundamentals of promises in Javascript and how to use it with the async/await syntax.

Promises

A promise is an object which contains promising code. Literally. It is code that will be fulfilled eventually - or throw an error. Promises let asynchronous functions behave like regular functions in the sense that they return a value. Except in this case, that value is in a “pending” state. It will eventually become fulfilled, or rejected. There are callbacks which can be used based on what the promise settles to:

  • .then(fulfillCallback, rejectCallback). This contains a callback for the promise when it gets fulfilled, but also if it is rejected.
  • .catch(rejectCallback). This is for handling the case where the promise gets rejected.
  • .finally(finalCallback). As the name suggests, this will execute when the promise settles to either be fulfilled or rejected.

It’s important to note that these can be chained as each of them implicitly returns a promise as well. This can be useful in certain scenarios, such as converting a response to an object in a fetch request, and then using that data:

fetch('https://totallycoolwebsitethatdoesnotexist.com')
  .then(response => response.json())
  .then(json => console.log(json))
  .catch(error => console.log(error));

Await keyword

Okay, so this is cool and all. But what if you don’t want to use a lot of callbacks, or what if you end up with a lot of nested callbacks that look messy? This is where the await keyword kicks in. It can be used so that our code waits until the promise is fulfilled before we move on to the next line. The code we have above will turn into something like this:

async function doSomething() {
  try {
    const response = await fetch(''https://totallycoolwebsitethatdoesnotexist.com');
    let json = await response.json();
    console.log(json);
  } catch (error) {
    console.log(error);
  }
}

doSomething();

You may run into this error:

Uncaught SyntaxError: await is only valid in async functions, async generators and modules

It’s important to note that await can only be used in asynchronous functions (functions that begin with the async keyword). Asynchronous functions implicitly return a promise, and this makes sense; the state of what the function returns will be fulfilled later - in this case, that later will happen when the fetch request is complete and we have parsed the response as an object. In other words, when the promise from the fetch request gets fulfilled as well as the json() method, then the asynchronous function’s return value (the promise) will be fulfilled (or possibly rejected) as well.

Parallel execution

Promise.all([promise1, promise2, …]) can be used to wrap multiple promises together in one. The promise returned from this fulfills when all of the promises get fulfilled, and rejects when one of the promises gets rejected. Here’s an example:

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, '1000');
});
const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000, '2000');
});
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, '3000');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
  console.log(`This promise took ${Math.max(...values)/1000} seconds to fulfill.`)
});

Loops

This is a problem that I’ve encountered when having to perform fetch requests one after the other. Passing an asynchronous function with the await keyword to a forEach() or a map() won’t work as anticipated, because they won’t wait for the promise to fulfill. Therefore they won’t actually execute in sequence. Let’s take a look at an example:

async function doSomething() {
    let arr = [4000, 3000, 2000, 1000]
    
    arr.forEach(async function(timeoutInMilliseconds) {
        await new Promise(function (res) {
            setTimeout(function () {
                console.log(timeoutInMilliseconds.toString())
                res()
            }, timeoutInMilliseconds)
        })
    })
    
}

doSomething()

We should expect this function to print

4000
3000
2000
1000

However, it prints

1000
2000
3000
4000

Let’s switch over to a for loop instead:

async function doSomething() {
    let arr = [4000, 3000, 2000, 1000]
    
    for (let i = 0; i < arr.length; i++) {
        let timeoutInMilliseconds = arr[i];
        await new Promise(function (res) {
            setTimeout(function () {
                console.log(timeoutInMilliseconds.toString())
                res()
            }, timeoutInMilliseconds)
        })
    }
    
}

doSomething()

And it works as expected!

Conclusion

Asynchronous programming allows our program to continue being responsive to the user while performing another task. Hopefully, this article covering promises and async/await in javascript will be useful to someone aspiring to be a web developer, or even just playing around with javascript in the browser or Node.js.