Patterns for Asynchronous Operations with async

Last Updated:
Image is “bike race!” by ((brian)) via Flickr; CC-BY-2.0

I remember the good old days when running asynchronous operations required you to use callbacks in an ever-deepening pit of “callback hell”. Those days aren’t completely gone, but it’s simple enough to wrap functions that use callbacks to make them use promises instead.

Oh yeah, promises. I remember the better good old days when promises became mainstream. I had worked with jQuery’s Deferred objects for quite a while before promises were common enough that I was allowed to pull a promise library into our projects at work, and at that point we had Babel, so I didn’t even need a promise library.

In any case, promises did as they promised (pun most definitely intended) for the most part and made asynchronous programming much more manageable. If you’re not really up to par with your promise skills, you can read more about promises here. Of course, they had their weaknesses. Many times you either needed to nest your promises or send variables into an outer scope because some of the data you needed was only available within a promise handler. For example:

function getVals () {
    return doSomethingAsync().then(function (val) {
        return doAnotherAsync(val).then(function (anotherVal) {
            // Here we need both val and anotherVal so we nested
            return val + anotherVal
        })
    })
}

// alernatively...

function getVals () {
    let value

    return doSomethingAsync().then(function (val) {
        // send val to the outer scope so others can use it
        value = val
        return doAnotherAsync(val)
    }).then(function (anotherVal) {
        // Here we grab value from outside
        return value + anotherVal
    })
}

Both of those examples are messy in their own way. Of course, they could be cleaned up a bit with arrow functions…

function getVals () {
    return doSomethingAsync().then(val => doAnotherAsync(val).then(anotherVal => val + anotherVal))
}

// alernatively...

function getVals () {
    let value

    return doSomethingAsync()
    .then(val => (value = val, doAnotherAsync(val)))
    .then(anotherVal => value + anotherVal)
}

That may clean up a bit and remove some of the syntactic cruft, but that doesn’t really offer any more readability. Thankfully, now we have gotten past those good old days and now we have async and await and we can avoid all that nonsense.

async function getVals () {
    let val = await doSomethingAsync()
    let anotherVal = await doAnotherAsync(val)

    return val + anotherVal
}

That just looks so much simpler and easy to follow. I am going to assume that you’re well aware of the existence of async and await so I won’t patronize you much with the details of how they work, but if you need a refresher you can read more about asyc/await on MDN. Here, we’ll be focusing on the patterns we’ve used in the past with Promises and how those patterns translate to async/await.

“Random” Sequential Asynchronous Operations

In the previous code snippets we technically already went over a pattern - “Random” Sequential Operations - which is what we will be talking about first. When I say random, I don’t really mean random. What I’m referring to is multiple functions that may or may not be related to each other, but are called separately. Put in another way, random operations aren’t the same operation(s) performed across an entire list/array of inputs. If you are still confused, you will see what I mean in the later sections when I go over the “non-random” operations and you will be able to see the difference.

Anyway, like I said, you have already been treated with an example of our first pattern. The operations are run sequentially, meaning the second operation waits until the first one is done before starting up. Of course, this pattern can be appear different that the example above when using promises assuming we don’t run into the situation that we did above where we need to pass multiple values forward to later operations:

function getVals () {
    return doSomethingAsync()
    .then(val => doAnotherAsync(val))
    .then(anotherVal => /* We don't need 'val' here */ 2 * anotherVal)
}

No need to access val in that final handler, so we can just chain then calls and not worry about passing values to an outer scope. The cool thing, though, is that the async/await version of the code doesn’t really change except that we use 2 * instead of val + in the final expression:

async function getVals () {
    let val = await doSomethingAsync()
    let anotherVal = await doAnotherAsync(val)

    return 2 * anotherVal
}

This is the type of situation where async/await excels: making a string of asynchronous calls act like they are synchronous. There are no little tricks, just straightforward “do this then do that” kind of code.

“Random” Parallel Asynchronous Operations

Alright, this time our operations are going to be run in parallel because none of the operations cares whether other operations are done yet, nor do they need a value from any other operations in order to be able to do their own work. When using promises, this is how it could be written (ignore the fact that I reused asynchronous function names but they are being used completely differently; they are just made up function names that make it obvious they are asynchronous; they are not necessarily the same functions used in earlier examples):

function getVals () {
    return Promise.all([doSomethingAsync(), doAnotherAsync()])
    .then(function ([val, anotherVal]) {
        return val + anotherVal
    })
}

We use Promise.all because it allows us to pass in any number of promises and once they have all been resolved, it’ll give us all the resolved values in one then handler. There are other options such as Promise.any, Promise.some, etc. depending on whether or not you’re using a promise library or certain Babel plugins and of course depending on your use case and how you want to handle the output or the potential for rejected promises. In any case, the pattern is very similar, you just choose a different Promise method and you receive different output.

The sad/great thing here is that async/await doesn’t allow us to move away from Promise.all or its constituents. It’s sad because async/await hides the use of promises in the background, but then we need to explicitly use Promise in order to do things in parallel. It’s great because it means we don’t need to learn anything new; we can just keep using what we know, but remove the extra characters required to pass a callback function into then. Instead, we can just await and pretend all those parallel operations took no time at all.

async function getVals () {
    let [val, anotherVal] = await Promise.all([doSomethingAsync(), doAnotherAsync()])
    return val + anotherVal
}

So async/await is about more than removing callbacks and unnecessary nesting, etc.: it’s about making asynchronous programming patterns look more like synchronous programming patterns so developers can wrap their minds around what the code is accomplishing more easily.

Iterating Parallel Asynchronous Operations

Here comes the operations that are not “random”. This is where we iterate over a set of values and perform the same asynchronous operation(s) over each value. In this parallel version, each element is being worked on at the same time. Here it is with promises:

function doAsyncToAll (values /* array */) {
    return Promise.all(values.map(doSomethingAsync))
}

Ok, so that’s as simple as it gets. How do you do it with async/await? Actually you don’t need to do anything! You could, but it would only make it more verbose:

async function doAsyncToAll (values /* array */) {
    return await Promise.all(values.map(doSomethingAsync))
}

That accomplishes absolutely nothing except adding a couple keywords that make you look like you’re being smart and using modern JavaScript, but in actuality you’re adding no value and the JavaScript engines will probably run this slower. If you get a little more complicated, though, async/await can certainly provide some benefit:

function doAsyncToAll (values /* array */) {
    return Promise.all(values.map(val => {
        return doSomethingAsync(val)
        .then(anotherVal => doAnotherAsync(anotherValue * 2))
    }))
}

That is not terrible, but async/await may be better at making it clear what exactly is happening here:

function doAsyncToAll (values /* array */) {
    return Promise.all(values.map(async val => {
        let anotherVal = await doSomethingAsync(val)
        return doAnotherAsync(anotherValue * 2)
    }))
}

Personally, I believe that is clearer, at least from within the callback to map, but some people may be confused here. When I first started using async/await I saw await inside the callback and it made me think that these callbacks were not being fired in parallel. This is one mistake that people can often make when using async/await in nested function and is one instance where it is potentially less simple to understand than using promises directly. However, a little exposure can help you more easily spot when you are using nested async functions and therefore they inner function is separate from the outer function and an await there does not pause the outer function.

Moving on from there, once you add more steps to your function, you increase the complexity of reading promises and add greater usefulness for using async/await.

function doAsyncToAll (values /* array */) {
    return Promise.all(values.map(val => {
        return doSomethingAsync(val)
        .then(anotherVal => doAnotherAsync(anotherValue * 2))
    }))
    .then(newValues => newValues.join(','))
}

Those multiple levels of then calls can really mess with a person’s head, so let’s implement this in the more modern way:

async function doAsyncToAll (values /* array */) {
    const newValues = await Promise.all(values.map(async val => {
        let anotherVal = await doSomethingAsync(val)
        return doAnotherAsync(anotherValue * 2)
    }))
    return newValues.join(',')
}

As usual, there are ways to slim this down, but slimming down isn’t the ultimate goal: readability and maintainability are, and that is generally where async/await comes in the handiest. It is often also simpler to write because we stay on the same synchronous paradigm as we are usually on.

Iterating Sequential Asynchronous Operations

We’re onto our final pattern. Once again we’re iterating over a list and applying the asynchronous operation(s) on each item in the list, but this time, we’re only going to do them one at a time. In other words, we can’t perform any operations on item two until we’ve finished our operations on item 1, etc.

function doAsyncToAllSequentially (values) {
    return values.reduce((previousOperation, val) => {
        return previousOperation.then(() => doSomethingAsync(val))
    }, Promise.resolve())
}

In order to accomplish the sequential order, we need to chain a then call off the previous operation. This could be done with reduce, but this is the most sane way of doing it. Note you that you need to pass in a resolved promise as the last argument to reduce so the first iteration has something to chain off.

Here, we can really see async/await shine again. We don’t need to use any array methods, such as reduce. We just need a normal loop and to call await from within it:

async function doAsyncToAllSequentially (values) {
    for (let val of values) {
        await doSomethingAsync(val)
    }
}

If you’re using reduce for a reason other than just to make the operations sequential, then you can continue using it. For example, if you’re adding results of all the operations together:

function doAsyncToAllSequentially (values) {
    return values.reduce((previousOperation, val) => {
        return previousOperation.then(
            total => doSomethingAsync(val).then(
                newVal => total + newVal
            )
        )
    }, Promise.resolve(0))
}

That just messes with my mind. It’s amazing how we end up in that callback hell even with promises again. Granted, especially since we’re using arrow functions, we could always just combine a lot of this onto one line, but that doesn’t actually help make it easier to understand. Using async/await makes it pretty clear, though:

async function doAsyncToAllSequentially (values) {
    let total = 0
    for (let val of values) {
        let newVal = await doSomethingAsync(val)
        total += newVal
    }
    return total
}

And if you still like using reduce when pairing your arrays down to single values…

async function doAsyncToAllSequentially (values) {
    return values.reduce(async (previous, val) => {
        let total = await previous
        let newVal = await doSomethingAsync(val)

        return total + newVal
    }, Promise.resolve(0))
}

Conclusion

The relatively new async/await keywords have really changed the game when it comes to writing asynchronous code. They help us to eliminate or minimize so many of the aspects of asynchronous code writing and reading that have plagued JavaScript developers for so long: callback hell, writing in a clear and more synchronous manner, and just being able to read and write asynchronous code in a manner that is simpler to understand. Because of this, it’s important to get a good understanding of how to use this new technology effectively, and I hope these patterns have helped you in that regard. God bless and happy coding.

 

Author: Joe Zimmerman

Joe Zimmerman has been doing web development ever since he found an HTML book on his dad's shelf when he was 12. Since then, JavaScript has grown in popularity and he has become passionate about it. He also loves to teach others through his blog and other popular blogs. When he's not writing code, he's spending time with his wife and children and leading them in God's Word. You can follow him @joezimjs.

We help JavaScript Developers Land Their Dream Job.

Current Open Roles

Your Princess (or Prince!) IS in this Castle – Node.js developer in Central London
London

Work with Node.JS and Ruby in the heart of Sydney
Sydney

Work with giant, caged robots, and Node.js; Senior Developer, West London
West London, UK

Want to help small family businesses thrive? Senior React/Redux Developer for a tech company enabling entrepreneurs
London

The only NodeJS job with a swimming pool; very exciting financial services company, City of London
London

Swap dreary grey skies for life in Bangkok - Senior Expat NodeJS Engineer in the Land of Smiles (offered in partnership with TechInParadise.com)
Bangkok

Sprechen Sie Deutsch und TypeScript fließend? Möchten Sie in London leben?
London

Senior React.js developer needed
London

Senior NodeJS Developer
London

Seasoned, highly-skilled React.js Developer looking for a satisfying challenge? Step right up.
London

Polygot JavaScript Developer with Go experience, and a desire to jump into other languages
London

Mobile App Developer Wanted for High-Growth Fundraising Platform
Kent

Make holidays happier: book yourself into this Senior Javascript Developer’s resort
London

JavaScript for High Performance; Senior Developer, London
London

It’s Not Rocket Science . . . Or is it?
London

Developer Wanted for High-Traffic Platform and (Disruptive?) Product
London

Can you help our client migrate to Node.js? Docklands, London
London

Big Developer Energy with Sassy Start-Up Vibe; Senior Node.js Developer in London
London

Aussie Start-up (Funded) Seeks Brilliant Developer
Sydney

And on the Seventh Day, You Rested
London

A JS job the banks don’t want you to have…
London