Notes on asynchronous JavaScript programming

I recently read up on JavaScript Promises and Sasha Vodnik’s “JavaScript: Async” helped me better understand its history and current usage.

Table of Contents

Synchronous and asynchronous programming

There are two ways code can be executed in JS:

  1. Synchronous refers to running each line of code and waiting for them to finish before the next. This idle time is also refered to as “blocking”.

    This is common for steps that need to be executed chronologically and depends on its predecessor.

  2. Asynchronous on the other hand does not wait for other lines of code to finish execution. They are immediately executed and JS provides ways to catch their respective responses after they’re done.

    Async code do not need others to execute independently hence the convenience of no blocking occurrences in its flow.

    Diagrams for synchronous and asynchronous processing Two diagrams showing queued execution of synchronous code versus immediate execution of asynchornous lines of code. 5 4 3 2 1 5 4 3 2 1

Ways to implement asynchronous programming in JS

XMLHttpRequest

This is the earliest way to communicate with server API with support starting from IE7. It is closely used with the AJAX (asynchronouse JavaScript and XML) technology.

let httpRequest = new HttpRequest();
httpRequest.open('GET', url);

httpRequest.onload = () => {
    if (httpRequest.status === 200) {
        successHandler(httpRequest.responseText);
    } else {
        failHandler(httpRequest.status)
    }
}

httpRequest.send();

In here I checked the response status in order to implement handling of successful and failed communication.

Promises

Starting in ES6, the Promises was shipped to browsers that enabled a new way to handle asynchronous acceptance (resolve) or rejection (reject) of responses.

function get(url) {
    return new Promise((resolve, reject) => {
        ...
        resolve(response);
        ...
        reject(response);
    })
}

get(url)
    .then(response => {
        successHandler(response);
    })
    .catch(status => {
        failHandler(status);
    })
    .finally(() => {
        ...
    });

Before I thought that the resolve and reject objects were direct equivalents of the successHandler and failHandler respectively from my examples and that the latter should substitute the first in code. Fortunately I now know better.

Chainable instance methods

  • then()
    • handles resolve cases and can be chained to other then()s to pass response objects.
  • catch()
    • handles reject cases.
  • finally()
    • executes after the first two regardless of the result.

Promise.all

In real projects we need to deal with multiple fetches of data in bulk! Good thing Promise.all is available for use.

It just takes an array of Promise objects and will resolve them in the chains.

Promise.all(urlArray.map(url => get(url)))
    .then(responses => {
        return responses.map(response =>
            successHandler(response);
        );
    })
    .then(data => {
        ...
    })
    .catch(status => {
        failHandler(status);
    })
    .finally(() => {
        ...
    });
  • Expect that it will return an array of responses and will need specific case handling based on your logic.
  • In my example there is a second then() with the data parameter. This represents an array of the individually-processed response objects in the initial then().

async/await

These two keywords are applied to functions that deal with asynchronous code or basically has Promises inside.

  • async is attached before the function declaration.
  • await is attached before methods that return a Promise to be resolved, usually during assignment. This keyword is only permitted in a method that is declared async and can be used multiple times.

In order to not mix Promise.all here I declared the individual get() calls to show only the two keywords in use.

(async function() {
    try {
        let responses = [];
        responses.push(await get(urlArray[0]));
        ...
        responses.push(await get(urlArray[n]));

        let data = response.map(response =>
            successHandler(response);
        );

        ...
    } catch (status) {
        failHandler(status);
    } finally {
        ...
    }
})();

Instead of the chained methods in Promise, the familiar try, catch and finally pattern is used here.

Dealing with array comprehension

We can mix these keywords with Promise.all() to take advantage of the new array methods! The code before can be simplified to:

(async function() {
    try {
        let responses = await Promise.all(urlArray.map(url => get(url)));

        let literals = responses.map(response => {
            return successHandler(response);
        });
        ...

I initially tried to do the following code but the push method is synchronous so it will fail to wait for the get() process.

urlArray.map(url => {
    responses.push(await get(url));
});

References

Twitter, LinkedIn