The long road to Async/Await in JavaScript

This is a comparison of different methods for performing asynchronous control flow in JavaScript, specifically Callbacks, Promises, Generators / Yields (ES6), and Async / Await (ES7). To follow along be sure you understand how the JavaScript Event Loop works and what it means when code is executed synchronously in the current stack, or shoved into the queue to be executed asynchronously in the future.

In the following contrived examples, publishLevel() is our main application code (perhaps something we’d see in a Controller), whereas getUser(), canCreate(), and saveLevel() are functions nested deeper in our application (perhaps in our Models).

Compatibility: Keep in mind that anything marked as ES6 will require Node.js >= 0.12 with the --harmony flag enabled, or any version of io.js >= 1.0. Browser support for ES6 is anything but spectacular and you’ll likely need to transpile. Anything marked as ES7 will definitely require a transpile for either Browser or Node.js.

Stage 0: Synchronous Code

This of course is not Asynchronous code but it does show us eloquent syntax. If you’re used to writing applications in, say, PHP, all of your code looks like this. This is the cleanest way one can write code and have it execute sequentially.

In the publishLevel() function we execute a function, get the result, pass it to the next function, run a branch, all in the current stack, all using minimal syntax.

Stage 1: Callbacks

This is the defacto approach used in Node.js applications where sequential asynchronous operations need to happen. As more function calls need to happen your code starts to nest even deeper. This phenomenon is affectionately known as callback hell.

What happens if you want to add another operation in the middle? You’ve got to re-nest all subsequent tasks within the new operation. Over time your code begins to flare outward, and those git-blame’s start to lie.

Promises everywhere is certainly more fun than Promises somewhere. If you’re planning on heavily using Promises with Node.js, check out mz, which will Promisify the Node.js API.

Stage 3: Generators/Yields (ES6)

ES6 gives us Generator functions which we can yield. When a function yields it is temporarily paused while the caller gets to do something with the yielded value. These were designed with doing iteration-based tasks in mind, yielding simple values, but here we’re going to yield Promises!

This is the first time we see code able to get executed in a different stack yet exist within the same function scope. Of course this new paradigm requires a new syntax. Generator functions have a * in their declaration, and we make use of the new yield keyword.

These can be used for doing control flow, but it’s really intended for iteration work, as you’ll see in this next example where I run a whole bunch of ugly code to manually keep the generator alive:

Notice this intimate knowledge we need to know about the function we’re calling! We take the result, pass it into another generator.next() call as an argument (this becomes the result of the yielded assignment).

Objects returned by generators have a .next() method, with a .done and .value attribute. If done is true, then the generators work is finished (the final return), however if done is false then there’s more work to happen (the preceding yield’s). The calling function is given the intermediate yield values, and has to know to continue the execution of the generator. While this is great for doing iteration work, it’s tedious from the perspective of doing asynchronous control flow.

Stage 4: Async/Await (ES7)

Async / Await is amazing, the mecca of working with asynchronous code in JavaScript. Personally I think it’s a shame we got Generators in ES6 instead of this. The solution is so eloquent that it will forever change the way we write JavaScript.

Internally it works with Promises. When the promise returned by an await resolves the code in the function will continue executing, and the resolved value will be provided.

I’m not sure how performant this code is. Certainly once JavaScript engines natively support Async / Await it’ll be fast, but the output from Babel I’m not too sure.

Stage 3.5: Generators/Yields + co (ES6)

I put this one out of order so you’d first see how awesome Async / Await is, and what the co module attempts to emulate.

Async / Await is great but we can’t use it today without complex transpilations! Generators are neat, but they require manual executions of .next()! Luckily there’s a sweet library called co which can sort of provide us the best of both worlds (transpile-free if you’re running modern Node). It’ll run .next() for us, and if we return Promises, when they resolve the generator will continue. Finally once the final return is called, that value is the final resolved value of the co-wrapped generator.

Related Posts

Thomas is the author of Advanced Microservices and a prolific public speaker with a passion for reducing complex problems into simple language and diagrams. His career includes working at Fortune 50's in the Midwest, co-founding a successful startup, and everything in between.

As someone who has finally embraced typescript I’m happy to say that async/await is as awesome as you say it is. Typescript is the way to write JS for me now. At first I was put off by it but Angular 2 embracing it pushed me to give it a real shot and see what was so great about it. I won’t be going back :)