In this article I will describe how the ES2017 async
functions are essentially a play between two older JavaScript features: generators
and promises
, both of which were added earlier to the language in the ES2016 specification.
promises
, generators
or async
functions.async
functions can be realised using promises
and generators
.async
functions are better or worse than the other approach.Since async
functions are now natively supported, what is the need to understand how they work?
Well, apart from the obvious reason of curiosity, an important reason is supporting older platforms. If you want your code using these newer features to run on older browser versions or older Node.js versions, you would be required to use tools like Babel to transform these newer features into older features.
Therefore, a solid understanding of how async
functions get decomposed into generators
and promises
can come in handy when reading/debugging your transformed code. For example, here is a simple example of an async
function :
This function gets transformed by Babel into the following ES2016 code (don’t worry about understanding it right now, we will cover it later) :
Babel output for the previous async function (ES2016)
They look really different! However, if you understand how async
functions actually work, then this transformation is fairly obvious.
Another fun fact, browsers also implement async
functions in a similar fashion i.e. they transform the async
code to use generators
and promises
quite similar to Babel.
Sometimes in order to understand how something works, the best way is to build it yourself. So let’s flip the question:
Imagine we are given a piece of code that uses
async
functions, how can we rewrite it using onlypromises
andgenerator
functions?
Here’s an async
function :
It performs three asynchronous tasks, one after the other where each task depends on the completion of the previous task. Finally, it returns the result of the last task.
Generators are functions which can be exited and later re-entered. Let’s quickly recap how they work. Here’s a simple generator function :
This generator-function gen
has some interesting aspects (lifted from the MDN docs) :
next
method.gen
is by calling the next
method on its iterator-object. Every time the next
method is called, its body is executed until the next [yield](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield)
expression. The value of this expression is returned from the iterator.next
method also accepts an argument. Calling it with an argument replaces the current yield
expression with the argument and resumes the execution till the next yield
expression.yield-by-yield
(i.e. one yield-expression at a time), by its iterator (the next
method).yield
has a give → halt → take behaviour, so to say.next
method is called again.next
method is called again, it takes the argument from it and replaces the currently halted yield-expression with it. It then moves to the next yield
.You may want to read the above summary again or refer to the amazing MDN docs!
By now you would be wondering, how do the generator functions help our situation? We need to model an asynchronous flow where we have to wait for certain tasks to finish before proceeding ahead. But so far in our discussion everything has been synchronous. How can we do that?
Well, the most important insight here is that the generator-functions can yield
promises
too.
A generator
function can yield
a promise
(for example an async task), and its iterator can be controlled to halt for this promise
to resolve (or reject), and then proceed with the resolved (or rejected) value. This pattern of weaving a an iterator with yielded promises
allows us to model our requirement like this :
(Notice how this generator function resembles our _async_
function!)
But this is only half the story. Now we need a way to execute its body. We need a function that can control the iterator of this generator
function to halt every time a promise
is yielded and proceeds once it resolves (or rejects). It sounds complicated, but is very simple to implement, as shown below :
A function that executes a generator function. (Only for explanation, do not use it !)
Now we can execute our generator
function init
using this runner
function as shown below:
Use `runner` to execute the body of `init`.
And that’s it! This combination of a runner
function and our generator
function achieves a similar outcome as the original async
function.
Please note that this runner
function is only for demonstrating the concept. It is not suitable for any serious use. If you are looking for a proper implementation, you can find it here.
We started with an async
function and then we wrote an identical implementation using generators
and promises
. That is, the following two pieces of code will have a similar effect :
async
code to ES2016 code using generators
and promises
. You can now revisit that transformed code and compare how our runner
function is similar to the _asyncToGenerator
function. In fact, that _asyncToGenerator
function is the foolproof version of our extremely simple runner
function.async
functions to ES2015 code i.e. without generators
. For this you would have to emulate generators
themselves (using a stateful busy loop with switch
cases for e.g. see the regenerator project).I hope this explanation clears up the mystery behind async
functions. They offer a simpler syntax and therefore less code noise. The proposal for async
functions states that :
Thanks to Akos, Alisa & Kristian for providing their feedback to improve this article.