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
By using Promises, a simple request to the GitHub API looks like this:
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
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.
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
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.
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?).
Now take a look at the same code, but this time using
async/await. The catch block now will handle every
JSON parsing errors.
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.
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
Pretty neat, huh? We have reduced the indentation level in two levels and turned it much more readable, especially by using an early return.
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:
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.
Using Async functions, though, we can just use a regular
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
forEach(), etc, because they rely on callbacks. The code block below would fail due these reasons.
But maybe you think something like this might work, after all, there’s an
async keyword prefixing the callback function, right?
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
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.
asyn/await, we can do this in a more straightforward way using the same
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.
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
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
This test always succeeds, because Mocha doesn’t wait until the assertions in the line
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).
Conveniently, Async functions always return Promises, which makes them perfect for this kind of unit test.
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.
async/awaitfunctions, returned Promises are not wrapped. That means a) returning a non-Promise value fulfills
pwith that value. b) returning a Promise means that
pnow 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
- You don’t need
awaitif you “fire and forget”. Sometimes you only want to trigger an asynchronous computation and are not interested in when it finishes.
- You can’t
awaiton 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
asyncfunctions to a
- You can do unit testing with
asyncfunctions using the test-framework Mocha.
- Sometimes you just don’t need to worry that much about unhandled rejections (be careful on this one).
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.
Async functions are an empowering concept that become fully supported and available in the ES8. They give us back our lost
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! :)