Patterns for Asynchronous Operations with async

Last Updated: Sep 19, 2019

Image is “The Bikers” by Olaf Gradin via Flickr; https://flic.kr/p/4Vravr , CC BY-SA 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