Upgrading to Async/Await with JavaScript ES8

Last week I wrote about Promises, a JavaScript feature introduced in ES6. Promises were a great leap forward in the escape from callback hell, but I’ve come to view them as something of a stop-gap now that async/await is supported in NodeJS (as of v7.6) and in the browser thanks to transpilers like babel.

Note: I’ll be using the latest and greatest JS syntax throughout this article, including template literals and arrow functions. You can check out a list of ES6 features here.

What’s so great about async/await?

Until now, writing asynchronous code in JavaScript has been awkward at best. To developers coming from Python, Ruby, Java, or really any other programming language, both callbacks and Promises will seem unnecessarily verbose, error-prone, and outright confusing.

Here’s the issue: to the programmer, there’s nothing terribly different about synchronous and asynchronous logic. There are plenty of performance and optimization issues the programmer should be thinking about when writing asynchronous code, but having radically different syntax for synchronous and asynchronous programming makes little sense.

To illustrate, here are three examples of the same logic — first using synchronous functions, then using callbacks, then using promises. Each one will retrieve details for the top story on Hacker News.

Here’s the (hypothetical) synchronous version:

This is nice and straightforward — nothing new for anyone who’s written code before. Just three steps: get a list of story IDs, get the details for the top story, and print the result.

But in JavaScript it’s important not to block the event loop, so if the storyIDs and details are coming from a file, a network request, a database, or any other expensive I/O operation, they should always be written asynchronously, using callbacks or Promises (that’s why the above code doesn’t work — in reality our HN client is driven by Promises).

Here’s the same logic using callbacks (again, hypothetical):

Oof. Now our steps are nested one inside the other, and we have to indent again for each step. If we had 20 steps instead of 3, the last step would have a 40-space indent! And if you want to add a new step in the middle, you need to add an indent to everything below it, creating huge, spurious diffs in Git. Also notice that we have to handle errors at every step along the way — there’s no way for us to group a set of operations in a single try/catch.

Let’s try again with Promises:

OK, this looks better. Our three steps are all aligned horizontally, and adding a new step in the middle is as simple as inserting a new line. That said, it’s still a little verbose with the Promise.resolve() and all the .then()s hanging around. Let’s see how it looks with async/await:

Much better! This looks just like our synchronous code, except with the await keyword thrown in. We’ve also surrounded our code with an anonymous async function to make the example more copy-pasteable (see below).

Under the hood, both hn.getStories() and hn.getItem() still return Promises, and won’t block the event loop while running. For the first time, we’re able to write asynchronous JavaScript using normal, imperative syntax!

Upgrading to async/await

So how can you start using async/await in your project? If you’re already using Promises, you’re ready to go! Any function that returns a Promise can also be called using await, which will cause it to return the resolved value. Callbacks, however, will need to be converted to Promises before you can use await.

Upgrading from Promises

If you were an early adopter of ES6 Promises, and your code currently uses chains of .then()s to execute asynchronous logic, upgrading to async/await is straightforward: just replace each .then() with an await.

You should also replace .catch() with standard try/catch blocks — finally we can use a single syntax for error handling in both synchronous and asynchronous contexts!

Last, note that await can’t go in the top-level of your module — it needs to be inside an async function.

Upgrading from Callbacks

If your code still uses callbacks, the best way to move forward is to Promisify your callbacks, then use await on any function that returns a Promise. See the “Promisifying Callbacks” section here.

Patterns and Gotchas

Of course, nothing new comes without growing pains. Here are a few patterns and pitfalls you might encounter when migrating to async/await:

Loops

One of the things I loved about JavaScript when I first started using it is the prevalence of function-passing. Sure, callbacks were messy, but I liked the syntax of Array.forEach over the traditional for loop:

If you’re using await though, Array.forEach won’t work, since it returns synchronously:

In this example, forEach will start a bunch of concurrent asynchronous calls to getItem() and return immediately, without waiting for the responses, so done! is the first thing that’s printed.

If you want to wait until the loop is finished, you either need a regular for loop (which will run in series), or a Promise.all (which will run concurrently):

Proper Optimization

The best part about async/await is also the biggest pitfall — you no longer have to think about writing asynchronous code, so you’ll be tempted to forget that small tweaks can have huge effects on performance.

As an example, say we want to get two Hacker News users and compare their karma. Here’s the naive implementation:

This works fine, but the second getUser() won’t run until the first call is finished. We should be running those calls concurrently!

Here’s a better solution:

Be sure to stay mindful of what can and can’t be done concurrently.

Wrapping Up

That’s it! I hope it’s clear just how huge an improvement async/await is over the status quo of asynchronous JavaScript. The ability to express asynchronous operations with the same syntax as synchronous operations has been a de facto feature in every multi-threaded language, and the fact that we now have the same liberty in JavaScript is a massive win for the JS community.

If you’d like to start playing with async/await, DataFire provides open source clients for services like Hacker News, GitHub, MongoDB, and Slack that are compatible with both Promises and async/await.