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:
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
:
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.
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/catch
es, 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 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:
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.
Using Async functions, though, we can just use a regular 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.
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 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.
Using asyn/await
, we can do this in a more straightforward way using the same Promise.all()
.
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 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()
.
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).
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.
- On
async/await
functions, returned Promises are not wrapped. That means a) returning a non-Promise value fulfillsp
with that value. b) returning a Promise means thatp
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/catch
es, at least at the top level. - For parallelism you can set your
async
functions to aPromise.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 return
s and try
/catch
es, 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! :)