A quick introduction to JavaScript concurrency

At some point in your life as a programmer, you’ll need to deal with code that runs asynchronously. Normally, code runs one command after the other. Nice and predictably, It goes from top to bottom, performing one command at a time, and not moving on to the next one until the one it’s working on is finished. This is usually great. It’s nice to be able to look at a list of commands and immediately understand what happens when — but sometimes commands might take a long time and/or take a lot of processing power, like a network request. When this happens, it’d be bad if those functions prevented the rest of the program from running, because then it’d be wasting time and memory that could be spent taking care of other things, and the whole program might even appear frozen.

This is where concurrency comes in. Instead of these long-running functions holding up your whole program, what if you could tell them to do their thing and get back to you when they’re done while the rest of your program works at the same time? This is certainly possible; it’s the recommended way to structure your script so it doesn’t waste any time or resources.

However, this introduces some issues. First of all, because scripts don’t run one line after another anymore, it becomes harder to understand what code executes exactly when. Second, it’s more difficult to pass any data generated back to the function that requested it. When these two problems combine, it can be frustrating to identify when some variables are modified and why. Error handling also becomes more difficult for the same reason returning data is more difficult: execution has already moved on by the time your asynchronous function has completed.

Let’s look at a concrete example. Without looking anything up, can you guess what the output of this code is?

function test(){

    console.log(1);

    setTimeout(() => { console.log(2); }, 0);

    console.log(3);

}

test();

I totally don’t blame you for guessing it’d be 1, 2 and then 3. That’s what it reads from top to bottom, so it’s perfectly reasonable to expect, but that’s not the output of this code. If you run it in your browser’s console, you’ll find it outputs 1, 3 and then 2. This happens because setTimeout returns immediately, regardless of how much work it has to do, and runs the function you passed to it later. Like I said before, this can be really useful, though it obviously can quickly become difficult to use.

Ye Olden Days

Historically, JavaScript has had a few ways of dealing with this. The oldest way is via callback functions. You’ve probably used them before if you’ve written more than the most basic JavaScript. In this convention, when you call an asynchronous function, you pass it another function as its last argument, which it calls when it’s done performing its asynchronous operation. It then passes your function the data it generated or an error as parameters. The function you give it is called a “callback” because it’s sort of like the asynchronous function is calling you back to let you know it’s done. Let’s see an example, in which we download data from a website:

download("https://example.com", (networkError, siteResponseInfo, body) => {

    // Operate on the data in body, handle errors, etc

});

This gets the job done. Your data is downloaded, you have access to it and you get to know about any errors that happened in the process. This method definitely isn’t perfect, though. For example, everything that requires data from the network is stuffed into a function that you need to pass to download, which likely doesn’t fit with the flow of the rest of your program very well. Furthermore, what happens if you need to do more asynchronous work after this data’s been downloaded?

Let’s see what that would look like. In this example, we’re building a simple server to download and cache example.com for a minute at a time. If we have data in the cache, we send that back to the person asking for it. Otherwise, we go download the contents of example.com, save it for the next person that asks for it, and send it back. Here’s a simple implementation of that with callbacks:

server.get("/", (serverRequest, serverResponse) => {

    cache.get(responseCacheKey, (cacheError, cachedValue) => {

        if(cacheError || !cachedValue){

            download("https://example.com", (networkError, siteResponseInfo, body) => {

                cache.set(responseCacheKey, body, 60); serverResponse.send(body);

                console.log("Downloaded from network");

            });

        }
        else{

            serverResponse.send(cachedValue);

            console.log("Responded from cache");

        }

    });

});

See how hard to follow that gets? This is what JavaScript developers refer to as “callback hell.” It’s where you need to perform numerous asynchronous operations that all depend on each other — the more you perform, the more your code gets indented and the harder to follow and understand your entire program becomes.

I Promise

ECMAScript 2015 has a solution to some of these problems, and they’re called promises. The primary issue they resolve is to allow asynchronous functions to immediately return a meaningful value instead of nothing at all. Instead of returning a value directly, an asynchronous function can instead return a Promise, which is sorta like a placeholder for a value while the asynchronous function figures out what the value is. A Promise has 3 possible states:

  • “pending” The value isn’t known yet. Check back in a little while!
  • “resolved” We’ve successfully figured out the proper value
  • “rejected” Our function has failed, and the proper value will never be known

So how does a Promise transform from a placeholder to meaningful data? We need to call a method called then on the promise, so that it knows what we want to do next. We pass then a callback function, whose first parameter will be the value that the promise resolves to. We should also call the catch method, which is just like then but handles errors instead of successes. Let’s see our function to download data from the network again, but this time with a Promise:

download("https://example.com").then((body) => {

    // Operate on the data in body here

}).catch((error) => {

    // React to any errors here

});

Doesn’t this look a bit clearer? We have an obvious separation of where we handle success and where we handle errors, we can return a value immediately so the overall flow of our program isn’t broken up too much — and we don’t have to shove all of our code into one huge callback function. Let’s see how this looks in the larger context of our caching server:

const responseCacheKey = "cache";

server.get("/", (serverRequest, serverResponse) => {

    function downloadFromNetwork(){

        return download("https://example.com").then((body) => {

            cache.set(responseCacheKey, body, 60);

            serverResponse.send(body);

        }).catch((error) => {

            serverResponse.send("<h1>There was an error</h1><p>Please try again later</p>");

        });

    }

    cache.get(responseCacheKey).then((cachedValue) => {

        if(cachedValue){

            serverResponse.send(cachedValue);

            console.info("Responded from cache");

        }

        else{

            downloadFromNetwork();
            console.info("Downloaded from network");

        }

    }).catch(downloadFromNetwork);

});

I don’t know about you, but I think this is alright. It’s definitely better than callbacks, because we never need to indent as far and our code is easier to read because we’ve flattened our asynchronous calls to be one after the other instead of nested within each other’s callbacks. We made a reusable wrapper function that returns a Promise for the data we download from the network, and we call it both in the case where we don’t have data cached and where the cache has a problem finding our data. Ultimately, this has boiled the core of our logic down to one area: if we have a value cached, send it to the person who asked for it — and if we don’t, then go get it from the network, save it and send it to that person.

This method isn’t great, but it’s better than what we had before. We still need to pass around callbacks and such, plus the divided scopes can make it difficult to access some variables. I bet we can do even better than this.

The Easy Way

Instead of calling asynchronous functions that immediately return and do work in the background while the rest of our program moves on, what if we could wait for them to finish, while still freeing up the CPU’s processing time and power? ECMAScript 2017 allows you to mark a function as async. An async function can be paused in the middle of its running to await the completion of another async function, which allows it to behave a lot like a normal, synchronous function. In order for your function to take advantage of this, you just need to declare it as async and have it return a Promise. For example, let’s set our function to download the contents of example.com once more:

const body = await download("https://example.com");

// body now contains the contents of the download

All that cruft and hassle we had to handle before boiled down to just one line. One line that’s easy to read, one line that’s easy to maintain, and one line that’s easy to understand. Let’s see how this this syntax feature transforms our whole application:


const responseCacheKey = "cache";

server.get("/", async (serverRequest, serverResponse) => {

    const cachedValue = await cache.get(responseCacheKey);

    if(cachedValue){

        serverResponse.send(cachedValue);

        console.log("Responded from cache");

    }

    else{

        try{

            const body = await download("https://example.com");

            cache.set(responseCacheKey, body, 60);

            serverResponse.send(body);

            console.log("Downloaded from network");

        }

        catch(error){

            serverResponse.send("<h1>There was an error</h1><p>Please try again later</p>");

        }

    }

});

Thanks to the power of async and await, all these concurrent functions behave entirely normally. We can read our program from top to bottom, just like you’d expect. While it’s still useful to understand all the features of raw promises and how you can use them to their fullest potential, using async and await syntax leads to clearer code that’s easier for anyone to understand and maintain. You just mark your function as async at the beginning and then you can await any asynchronous operations within your function, while it all still looks like normal, synchronous code.

Next time you find yourself tearing your hair out, debugging a big triangle of nested callbacks, think about converting your asynchronous code to use promises. Your code will still look more like code you’re used to writing and it’ll help you reason out your order of operations, even if it involves a lot of asynchronous calls.

If you’d like to see all three of these examples in action, you can check out a full demo of them on GitHub. Note that while the examples are meant to run on Node.js, all the concepts discussed here are fully applicable in modern browsers, too.


Michael Hulet is an independent software developer from Nashville who specializes in iOS, watchOS, tvOS and the front and back ends of the web, but really just enjoys software in general. He’s also into data security, hockey (especially the Nashville Predators), and anywhere that has a good plate of barbecue. He’s looking for work right now, especially if your company is in Sweden. You can find him online at his website, on Twitter and at GitHub.

 

Take your JavaScript skills to an even higher level with our Techdegree in Full Stack JavaScript.
Free 7-day Techdegree Trial