Synchronize your asynchronous code using JavaScript’s async await

Introduction

Since the ECMAScript 2017 (ES8) release and its support adoption by default on Node.js 7.6, you no longer have excuses for not being using one of the hottest ES8 features, which is the async/await. The intent of this article is to show you a bunch of reasons with examples of why you should adopt it immediately and never look back.

But first of all, since Promises are the foundation of Async functions, to be able to grasp the contents of this article, you will need a reliable knowledge about Promises and at least awareness about Generators as well. In this article, we won’t cover in depth both features usage and functionalities, but for really understanding how it works, I strongly recommend this Ponyfoo series, which perfectly covers everything that you must know about Promises, Generators, and more.


Let’s get started

Promises landed on JavaScript as part of the ECMAScript 2015 (ES6) standard, and at the time of its release, it changed the way developers use to write asynchronous code. One of the most significant Promises achievements is that it considerably reduced the complexity of the asynchronous code, improving legibility, besides helping us to escape the pyramid of doom (also known as callback hell).

By using Promises, a simple request to the GitHub API looks like this:

Request example using Promises

OK, I have to admit that it is quite clear and for sure makes understanding more accessible than when using nested callbacks, but what if I told you that we could write asynchronous code like this, by using async/await:

Request example using async/await

It’s simply readability at its top. You can identify each step of the process in a clear way, just like if you have been reading a synchronous code, but it’s entirely asynchronous! Even in the contrived example above, it’s clear we saved a decent amount of code. We didn’t have to write .then, create an anonymous function to handle the response, or to give a response name to a variable that we don’t need to use — and we also avoided nested code.

The small advantages add up quickly, which will become more evident in the following code examples. So, let’s jump into Async functions implementation.


Async functions

Any Async function returns a Promise implicitly, and the resolved value of the Promise will be whatever returns from your function. Our function has an async keyword on its definition (which says that this function will be an Async function, of course). It also has an await keyword, which we use to “wait for” a Promise. This also implies that we can only use await inside functions defined with the async keyword.

Simple example using async/await

If the Promise resolves, we can immediately interact with it on the next line. And if it rejects, then an error is thrown. So try/catch magically works again.

Simple example using async/await with try/catch

That allows us to write code that looks synchronous at a first sight but is asynchronous under the hood, and that’s the best part about async/await. That is where all its power lies. The fact that the API returns a Promise instead of blocking the event loop is just an implementation detail. And since Node.js 8 has a new utility function which converts a callback-based function into a Promise-based one, called util.promisify(), we are pretty covered for using Async functions even working with “legacy” code.


Be responsible (error handling)

There are few issues that I have been through into while playing with this, so it’s good to be aware of them.

With Great Power Comes Great Responsibility — Benjamin Parker

One of the most insidious problems while working with Async functions is that you have to be careful since errors are “silently” swallowed (!!) within an Async function — just like inside standard Promises. Unless we add a try/catch, blocks around our await expressions, uncaught exceptions — regardless of whether they were raised in the body of your Async function or while it’s suspended during await, will reject the promise returned by the Async function.

In the example below which we use Promises, the try/catch won’t handle if JSON.parse fails because it’s happening inside a Promise. We need to call .catch on the Promise and duplicate our error handling code, which will (hopefully) be more sophisticated and elegant than a console.log in your production-ready code (right?).

Bad example of error being handled using Promises

Now take a look at the same code, but this time using async/await. The catch block now will handle every JSON parsing errors.

Example of error being handled using async/await

My advice is to ensure that your async functions are entirely surrounded by try/catches, at least at the top level.


Dealing with conditionals

Consider a code block like the code below which fetches some data and decides whether it should return that or get more details based on some value in the data.

Conditional example using Promises

Just looking at this gives you chills. It’s easy to get lost in all that nesting (6 levels), braces, and return statements that are only needed to propagate the final result up to the main Promise.

This example becomes way more comprehensible when rewritten with async/await.

Conditional example using async/await

Pretty neat, huh? We have reduced the indentation level in two levels and turned it much more readable, especially by using an early return.


Loops

Async functions get really impressive when it comes to iteration. For instance, let’s say that we want to insert some posts into our database, but sequentially. That is, we want the Promises to execute one after the other, not concurrently. By using Promises, we’d have to roll our Promise chain. Consider the below example which illustrates that:

Loop example using forEach and Promises

The example above works, but for sure is unsightly. It’s also error-prone, because if you accidentally do something like the code block below, then the Promises will execute concurrently, which can lead to unexpected results.

Second example using forEach and Promises

Using Async functions, though, we can just use a regular for…of loop.

Example using async/await and for.. of loop

Oh, but note that you cannot use any loop forEach() loop here. Why? Well, that’s simple. That happens because that await only affects the innermost Async function that surrounds it and can only be used directly inside Async functions. That is a problem if you want to use one of the Array.prototype utility functions such as map(), forEach(), etc, because they rely on callbacks. The code block below would fail due these reasons.

Example using forEach with async/wait

But maybe you think something like this might work, after all, there’s an async keyword prefixing the callback function, right?

Second example using forEach with async/await

Unfortunately not. Again, this code doesn’t work, but there is one caveat: the Promise returned by db.insert() is resolved asynchronously, which means that the callbacks won’t finish when forEach()returns. As a consequence, you can’t await the end of insertPosts().

Resuming: the whole idea here is to just not await in callbacks.


Sequentially vs. Parallelism

It’s important to note that, even using Async functions and your code being asynchronous, it’ll be executed in a serial way, which means that one statement (even the asynchronous ones) will execute one after the another. But since Async functions become Promises, we can use a workflow so as we would use for Promises to handle parallelism.

Remember that with Promises we have Promises.all(). Let’s use it to return an array of values from an array of Promises.

Example showing concurrence with Promises

Using asyn/await, we can do this in a more straightforward way using the same Promise.all().

Example showing concurrence with async/await

Simple as that. Note that the most important parts are, firstly, creating the Promises array, which starts invoking all the Promises immediately. Secondly, that we are awaiting those Promises within the main function. Consider the code block below, which illustrates three different Promises that will execute in parallel.

Second example showing concurrence with async/await

As the first example, first we create an array of Promises (each one of the get functions are a Promise). Then, we execute all of them concurrently and simultaneously, awaiting for all of them to finish (await Promise.all). Finally, we assign the results to the respective variables users, categories and products. Despite the fact that it works, it’s important to say that using Promises.all() for everything is a bad idea.

For a better understanding of how it works, you must be aware that if one of the Promises fail, all of them will be aborted, what will result — in our previous example — to none of these three variables receiving their expected values.


Unit testing with async functions

By using Async functions you can even apply unit tests to your functions. The following code uses the test-framework Mocha to unit-test the asynchronous functions getUsers() and getProducts().

Unit test example with Promises

This test always succeeds, because Mocha doesn’t wait until the assertions in the line B and C execute. You could fix this by returning the result of the Promise chain, because Mocha recognizes if a test returns a Promise and then waits until that Promise is settled (unless there is a timeout).

Second unit test example with Promises

Conveniently, Async functions always return Promises, which makes them perfect for this kind of unit test.

Unit test example with async/await

Quite simple, huh? There are thus two advantages to using Async functions for asynchronous unit tests in Mocha: the code gets more concise and returning Promises is taken care of, too.


Quick tips and must remembers

  • Async functions are started synchronously, settled asynchronously.
  • On async/await functions, returned Promises are not wrapped. That means a) returning a non-Promise value fulfills p with that value. b) returning a Promise means that p now mirrors the state of that Promise.
  • You can forward both fulfillment and rejections of another asynchronous computation without an await. That means that you return values which can be handled by another async function.
  • You don’t need await if you “fire and forget”. Sometimes you only want to trigger an asynchronous computation and are not interested in when it finishes.
  • You can’t await on callbacks, since it can bring you a lot of bugs.
  • Your Async functions must be entirely surrounded by try/catches, at least at the top level.
  • For parallelism you can set your async functions to a Promise.all method.
  • You can do unit testing with async functions using the test-framework Mocha.
  • Sometimes you just don’t need to worry that much about unhandled rejections (be careful on this one).

Using async/await today

As pointed at the very beginning of this article, Node.js 7.6 was released a few months ago (and Node.js 8, which is a major version, was released just a few weeks ago), bringing us default support and coverage for async/await. That means that the feature is no longer considered experimental and we don’t need to use compilers such as Babel, or the — harmony flag, which are almost-completed features that are not considered stable by the V8 team. So all you just need to do is installing Node.js 8 and enjoy all power which async/await brings us.


Conclusion

Async functions are an empowering concept that become fully supported and available in the ES8. They give us back our lost returns and try/catches, and they reward the knowledge we've already gained from writing synchronous code with new idioms that look a lot like the old ones, but are much more performative.

And the good part is that even Node.js 8 still not being an LTS release (currently it’s on v6.11.0), migrating your code base to the new version will most likely take little to no effort.


Special thanks to everyone who helped me to review drafts of this article. You’re amazing! :)