Some of us like to brag about how old we are because we started working on languages like Assembler or Fortran or Basic. This was a while ago, when computers were very simple, and although those languages were extremely cryptic and they made us productive, it's also a reality that we were doing a lot less with them. We were building much shorter buildings. As time moved on, computers became a lot more powerful, and our customers demanded that we build skyscrapers. You might have heard of something called Moore's law, which is the observation that the number of transistors on an integrated circuit will double every two years with a minimal rise in cost. Yes, computers have become very powerful, but just due to basic physics and energy density issues, we've also hit a wall in absolute computing power that we can pack within a single processor. As a result, the world started moving toward multiple cores, and multiple processors; even your phone - or maybe even your watch - now has multiple cores or multiple processors inside it.

When these multiple cores or multiple processes try to work together, adding two processors doesn't always equal 2X the performance. Sometimes it can even be less than 1X because the competing processors might be working against each other. For sure, the benefit you get will be less than 2X because some overhead is spent on coordination. Now imagine if you have a 64-core processor, how would that look? And how would you write code for it? There will always be that one really smart guy on your team that understands the difference between mutexes and semaphores, and that smart guy will act like the cow that gives one can of milk and tips over two. His smarts will make rest of the team unproductive because, let's be honest, these concepts can be hard to understand, harder to write, and very hard to debug.

Although it's no surprise that as we're building more complex software, our platforms and languages have also evolved to help us deal with this complexity, so the entire team of mere mortals is productive. Languages have also evolved to support more complex paradigms, and JavaScript is no exception.

In this article, I'm going to explore a back-to-basics approach by explaining asynchronous programming in JavaScript. Let's get started.

A Little Pretext

Before I get started, there's a little challenge I must deal with. Demonstrating asynchronous concepts through text and images as they appear in this article can be difficult. So I'm going to describe the various concepts, but you should also grab the associated code for this article at the following URL: https://github.com/maliksahil/asyncjavascript. I recommend running the code side-by-side as you read this article as that will help cement the concepts.

Let me first start by describing the code you've cloned here.

Code Structure

The code you've cloned is a simple nodejs project. It uses ExpressJS to serve a static website from the public folder, as can be seen in Figure 1.

Figure 1: The project structure
Figure 1: The project structure

To run it, just follow the instructions in readme.md. At a high level, it's a matter of running npm install, and hitting F5 in VSCode. Additionally, you'll see that in index.js as seen in Listing 1, in addition to serving the public folder as a static website, I'm also exposing an API at the “/random” URL. This API is quite simple; it waits for five seconds and returns a random number. I have a wait here to demonstrate what effect blocking processes, such as this wait, can have your browser's UI. The reason I've written this code in NodeJS is because I could use identical code for the wait on both client and server, although this isn't a hard requirement.

Listing 1: The index.js server side file

const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.static('public'));
app.get("/random", (request, response) => {
    waitForMilliSeconds(5000);
    const random = {
        "random": Math.floor(Math.random() * 100)
    };    
    response.send(random);
});

app.listen(PORT, () => console.log(`Server listening on port: ${PORT}`));

function waitForMilliSeconds(milliSeconds) {
    var date = new Date();
    var curDate = null;
    do { curDate = new Date(); }
    while (curDate - date < milliSeconds);
}

Let's also briefly examine the client side code. The index.html file is quite simple. It references jQuery to help simplify some of the JavaScript I'll write. It has a button called btnRandom that calls a JavaScript. It has a div called “output” where the JavaScript can show messages to communicate to the user. The idea is that I'll call a function that blocks for five seconds, and I'll show a “start” and the random number output message when the function starts and then when it's done.

Additionally, I've placed a text area where users can type freely. The function takes five seconds to complete, so what I'd like you to try is, within those five seconds, try to type in that text area. If you can type in that text area while the function is executing, that's a non-blocking UI, which is a good user interface. But if the UI is frozen and you cannot type in that textbox while your function runs, that's a bad user experience.

The user interface of my simple HTML file looks like Figure 2. The index.html file can be seen in Listing 2.

Figure 2: The user interface
Figure 2: The user interface

Listing 2: index.html

<html>

<head>
    <script 
        src="https://code.jquery.com/jquery-3.7.1.min.js";
        integrity=".." 
        crossorigin="anonymous">
    </script>  
</head>

<body>
  Press button to make async call:
  <button type="button" id="btnRandom">Click</button>
  <br />
  <div id="output"></div>
  <script src="scripts/1.sync.js"></script>

  <br/>
  <textarea rows="5" cols="40">Try typing here 
   while the long running call is running ..</textarea>
</body>

</html>

A Synchronous Call

Let's first start by writing a simple JavaScript function that takes five seconds to execute. At the end of five seconds, it simply returns a random number. This function is basically the same function you see in index.js called “waitForMilliSeconds”, except that for now, I'll just run it client side, and the function itself will return a random number.

The function is called on the click event of the button you see in Figure 2. As soon as the user clicks on the button, it shows a “Start” message in the output div. Then the function runs for five seconds and five seconds later, you should see a random number shown in the output div. The code for the synchronous call is refenced in index.html as scripts/1.sync.js and can be seen in Listing 3.

Listing 3: 1.sync.js client side synchronous JS

$("#btnRandom").on("click", function () {
    $("#output").text("Start");
    randomNum = waitForMilliSeconds(5000);    
    $("#output").text(randomNum);
});

function waitForMilliSeconds(milliSeconds) {
    var date = new Date();
    var curDate = null;
    do { curDate = new Date(); }
    while (curDate - date < milliSeconds);
    return Math.floor(Math.random() * 100);
}

Go ahead and run npm start (or F5 in VSCode) and visit the browser at http://localhost:3000. Click the “Click” button from Figure 2, and immediately try typing in the text area below. What do you see?

You'll notice that until the call completes and the random number is shown in the output div, the page is essentially frozen. It accepts no input from the user. In fact, the page is dead: It accepts or responds to no events. This is certainly a bad user experience but may also lead to inexplicable bugs.

Callbacks

Now let's explore a technique in JavaScript called callbacks. If you remember what you first did, this line stands out:

randomNum = waitForMilliSeconds(5000);

This means that the returnvalue of waitForMilliSeconds is what gets populated in randomNum.

We've learned from other languages that you could pass in a function pointer to waitForMilliSeconds. Wouldn't it be nice if waitForMilliSeconds could call that function pointer when it's done with its five seconds of blocking work?

To facilitate that, modify your waitForMilliSeconds function, as shown below in the next snippet. The login has been trimmed for brevity and the only change is that instead of sending back a return value, you're now accepting a parameter called callbackFunc. When you're done with your work, you simply call this callback function and pass in the result.

function waitForMilliSeconds(milliSeconds, callbackFunc) {
    var date = new Date();
    ..
    random = Math.floor(Math.random() * 100);
    callbackFunc(random);
}

Accordingly, how you call this method also changes. This can be seen below.

waitForMilliSeconds(5000, (random) => {
    $("#output").text(random);
});

As you can see, you're now calling waitForMilliSeconds with two input parameters. The first parameter instructs the function to wait for five seconds and the second is an anonymous function parameter. This function gets called once waitForMilliSeconds is done and it calls the callbackFunc variable function.

Before you run this, what do you expect the behavior will be? Will it block the UI or not? Let's find out. Go ahead and run this. You'll notice that although the code seems to have a different structure, the callback seemed to have no effect on the single-threaded nature of the code. The UI still blocks.

Bummer!

Well, at least you learned a new concept here, and that such callbacks have no effect on the single-threaded nature of execution.

Promise

JavaScript has yet another way of structuring your code, which is Promises. You may have seen them when writing AJAX code, where your code can make an HTTP call to the server without refreshing the whole page. This is how complex apps such as Google Maps were born. Before Google Maps, navigating a map required you to refresh the whole page. It was a horrible user experience, until someone showed us a better way. Technically speaking, Outlook for the web was leveraging this technique already, but hey, this isn't a race.

There's also pure client-side code. What if you were to use Promises instead of callbacks. Will the code not be single threaded then? Let's find out.

The idea behind a JavaScript Promise is that the function doesn't return a value, but instead returns a Promise. The Promise will either resolve (succeed) or reject (fail). When it resolves, it can send back a success output. If it fails, it can send back an error.

Your caller then uses standard paradigms around Promises to handle success with a Then method.

Let's modify the waitForMilliSeconds method to now return a Pomise and resolve it on success. You can see this method in Listing 4.

Listing 4: Using Promises

function waitForMilliSeconds(milliSeconds) {
    const myPromise = new Promise((resolve, reject) => {
        var date = new Date();
        ...
        random = Math.floor(Math.random() * 100);
        resolve(random);
    });
    return myPromise;
}

This allows you to write calling code:

waitForMilliSeconds(5000).then( (random) => {
    $("#output").text(random);
});

Let's run this code again and hit the Click button. What do you see?

Ah! Yet again, although the code functionally is accurate, it still blocks the UI thread. The code is still single threaded. It responds to no input while the function is running and suddenly reacts to key strokes queued up in those five seconds.

Awful! Promises and lies. I guess that didn't solve the problem either.

XHR

At this point, you must be thinking that you've written so much AJAX code, and that code leveraged Promises, callbacks, and other paradigms. For sure you didn't see that blocking behavior there. What is so special about AJAX that it doesn't block?

There are many ways to write AJAX code. I've referenced jQuery and certainly jQuery has abstractions that help write AJAX code. The most basic way to call AJAX is by using XHR.

The way XHR works is that you instantiate a new instance of XMLHttpRequest. You subscribe to the loadend event and fire your HTTP request. Now whenever the call returns, the loadend event gets called and you get the results. You can process accordingly whether it's an error or success.

Let's start by instantiating the XMLHttpRequest.

const xhr = new XMLHttpRequest();

Before you send the request, let's subscribe to the loadend event. You're going to call an anonymous method when the event gets called, which shows whatever response the server sent.

xhr.addEventListener("loadend", () => {
    $("#output").text(xhr.responseText);
});

Next, let's send the request.

xhr.open("GET", "/random");
xhr.send();

You can see the final code that puts all this together in Listing 5.

Listing 5: Simple XHR request

$("#btnRandom").on("click", function () {
    $("#output").text("Start");
    const xhr = new XMLHttpRequest();

    xhr.addEventListener("loadend", () => {
        $("#output").text(xhr.responseText);
    });

    xhr.open("GET", "/random");

    xhr.send();
    $("#output").text("Sent xhr request");
});

Remember from Listing 1, the server-side code is basically the same code you've been using except now instead of running on the client, it's running on the server. It waits five seconds and sends back a random number.

Now go ahead and run this by referencing this script, pressing F5 to refresh the browser, and clicking the button.

Very interestingly, now the UI doesn't block. How odd is that?

Although this is great, wouldn't it be nice if complex client-side code could be afforded the luxury of being multi-threaded? This XHR-based code feels so complicated. My example was simple, but imagine how this could look with multiple dependencies, inputs dependent on other XHR calls succeeded, timing issues, etc. Ugh!

Promises and XHR

Let's start by cleaning this code up a bit. You've already seen Promises in action. Can you combine XHR and Promises together to help write code that's simpler? Sure! All you'd have to do is abstract out all this XHR code into its own method that returns a Promise. When you do an xhr.send(), just return the Promise. When XHR's loadend event is called, either resolve or reject the Promise as per the return results. This can be seen in Listing 6.

Listing 6: XHR and Promises

function makeXhrCall() {
    const myPromise = new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.addEventListener("loadend", () => {
            resolve(xhr.responseText);            
        });
        xhr.open("GET", "/random");    
        xhr.send();
      });
    return myPromise;
}

By doing so, the calling code becomes a lot simpler, as can be seen below.

makeXhrCall().then((output) => {
    $("#output").text(output);
});

Now go ahead and run this code. It runs just like before and it doesn't block the UI thread. Is this because you're using a Promise or that you're using an XHR? Well, you did use a Promise on a loop that was entirely on the client side and that did block the UI. This non-UI blocking magic is built into XHR.

Async Await

Recently, JavaScript introduced support for async await keywords. The Promise code looks cleaner than pure XHR code, but it still feels a bit inside out. Imagine if you had three Promises you needed to wait for and those inputs go into two more Promises, which finally go into another AJAX call? Luckily, Promises do have concepts such as resolveAll etc., which do help. They're an improvement over pure XHR code. However, the code becomes severely indented and you're stuck in a hell hole of brace matching and keeping your code under 80 characters width.

Async await helps you tackle that problem. Look at the sync code example from Listing 3. To convert that from sync to async, all you have to do is add the async keyword in front of the function. In other words, change this line of code:

function waitForMilliSeconds(milliSeconds) {

into this line of code:

async function waitForMilliSeconds(milliSeconds) {

That's it! Okay, I lied. Your calling pattern changes slightly too, but for the better. Your calling code changes like this:

$("#btnRandom").on("click", async () => {
    $("#output").text("Start");
    random = await waitForMilliSeconds(5000);
    $("#output").text(random);
});

The changes aren't severe at all. All you had to do was follow two rules:

  • Put await in front of a method call that is intended to be async.
  • You cannot use await in any method that isn't async itself.

Now you can finally start writing async code that doesn't look like a severely indented case of brace-matching disease. You can extrapolate this to an XHR example, as can be seen in Listing 7.

Listing 7: Async XHR calls

let makeXhrCall = async () => {
    const myPromise = new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.addEventListener("loadend", () => {
            resolve(xhr.responseText);            
        });
        xhr.open("GET", "/random");    
        xhr.send();
      });
    return myPromise;
}

Now when you run this code, although it's quite simplified, unless it's an XHR call, it still seems to block the UI.

Async Await with Workers

What's so peculiar about XHR that it doesn't block the UI thread? It seems to work on an Eventing model. Luckily for you, you can leverage that same capability using workers in JavaScript. Workers in JavaScript is a topic in itself, but for my purposes here, you need to separate out the blocking client-side code in its own worker, which, in this case, means its own JavaScript file.

This worker will now listen for, and respond to, messages after the work is done. So go ahead and create a new file under the Scripts folder called hardwork.js. Here's where you intend to run your hard-working loop that takes five seconds to complete.

Inside this hardwork.js, you first need to add an event listener for “message”. This looks like:

addEventListener("message", (message) => {
    if (message.data.command === "Start") {
        waitForMilliSeconds(message.data.milliSeconds)
    }
});

As you can see, you're adding an event listener for “message”. Whenever this is called, you can assume that some input parameters are sent to you, in this case, via message.data.command, which tells you what action to take. The code is pretty simple, so you just look for “Start”. Additionally, the data object has another property called milliSeconds that tells you how long to wait before returning a random number.

As you will see shortly, you have full control on the data property and you can define your own structure. Let's get to that in a moment. First let's focus on what waitForMilliSeconds looks like in this worker world.

Below is an abbreviated version of waitForMilliSeconds that communicates back to the caller via the postMessage method.

function waitForMilliSeconds(milliSeconds){
    ...
    random = Math.floor(Math.random() * 100);
    postMessage(random);
}

I removed the actual logic for brevity, but it's the same waitForMilliSeconds function I've used numerous times in this article already. That's it. This is what the worker looks like.

Now let's see how calling this worker looks.

As can be seen in Listing 8, you first start by creating a worker object using the hardwork.js file. Then, in the doHardWork method, post a message with a custom object structure, which is made available as the message.data property to the worker. I think “add a listener” for “message” and whenever a message is available, which is after the five second loop, I grab the random number output and resolve it. I think to use the async await pattern to set the output div's value.

Listing 8: Async Await with Workers

const worker = new Worker('./scripts/hardwork.js');

$("#btnRandom").on("click", async () => {
    $("#output").text("Start");
    random = await doHardWork();
    $("#output").text(random);
});

async function doHardWork() {
    return new Promise((resolve) => {
        worker.postMessage({ 
            command: "Start", "milliSeconds": 5000 });
        worker.addEventListener("message", (output) => {
            resolve(output.data);
        });
    });
}

Wait a minute. This looks quite similar to the XHR object, doesn't it? Over there also, you had an addEventListener, only the actual event was different.

Go ahead and run this code. Remember that this is a client-side loop. Go ahead and click the button. The code behaves as before, but now the UI thread no longer locks. The text area remains responsive while the loop is running without XHR.

Great. You've finally achieved the panacea of clean code, that doesn't block the UI thread.

Summary

As computers become increasingly powerful and complex, it's reasonable to assume that we're going to have to write increasingly complicated code. To write that complicated code, we're going to have to learn new patterns, such as the asynchronous patterns available in JavaScript.

In this article, I discussed many such patterns, and I built a story bit by bit to show you how you can use the various paradigms in JavaScript to create non-blocking code that's easy to maintain.

There's a lot more to learn, of course, and I'm sure, as time moves forward, greater demands and better patterns will emerge.

This is the beauty of our industry. Never a boring day.

Until next time, happy coding