On my last post we saw the theory behind generators in JS/TS. In this article I will apply those concepts and show how we can use generators to build something similar to async/await. In fact, async/await is implemented using generators and promises.
First we will show how we can deal with asynchronicity using generators by writing an example with callbacks.
The idea is as follows. When using callbacks, we pass some function that will be called whenever the async action has finished. So what if we don't call a callback, but instead call
next on some generator? Better yet, what if this generator is the code that called our async function? That way we would have a code that calls some asynchronous process, stays paused while the asynchronous process isn't finished and return its execution whenever it is ready. Check this out:
If you don't know what is
...args in the implementation above, take a look at spread syntax.
We wrap our asynchronous operations with
asyncWrapper. This wrapper just pass a callback to give control back to the generator
main whenever the async process is completed. Notice how our code in main looks totally synchronous. In fact, just looking at
main, we can't assert if there is anything asynchronous at all, although the
yield gives a hint. Also notice how our code is very similar to what it would have been with
async/await, even though we don't use
Promises. This is because we are abstracting away the asynchronous portions from our consuming code
Using callbacks like above is fine, but there are some problems.
- The code feels weird. Why should
mainshould be able to just call the async operation and everything should be handled in the background.
- Where would we do error handling?
- What if the asynchronous operations calls the callback multiple times?
- What if we wanted to run multiple async operations in parallel? Since a yield corresponds to a pause in execution, we would need to add some complicated code to decide if when we call
nextis it to execute another operation or is it because an asynchronous operation has finished?
- We have the problems that normal callbacks do (callback hell, etc).
We can solve the problems above utilizing Promises. We will begin with a simple implementation with only one yield and no error handling and then expand it.
First we need to make our asynchronous operation
addAsync return a promise, we will deal with the case that it doesn't later.
To solve 1, we need to change our wrapper to receive the code that we want to execute, becoming a runner. This way our runner does the things it needs and gives control back to our code whenever it is ready, while hiding how anything works from our code. The runner needs to do essentially two things:
- Initialize our code.
- Take the promise that is yielded to it, wait for its fulfillment and then give control back to our code with the resolved value.
And that's it! The problem 3 from our list is automatically solved whenever we use promises. The full code is the following:
Let's walk through the execution.
- First we call our runner with the
- The runner initializes our generator and then calls
it.next(). This gives control to
- Main executes until the
yield. It yields the return value of
addAsync, which is a promise. This promise is unfulfilled at the moment.
- Now the control is with the runner. It unwraps the value from the generator yield and gets the promise. It adds a
.thenthat will pass the value of the fulfilled promise to
- Whenever the promised is resolved and the runner gives control to
main, the yield expression evaluates to the resolved value of the promise (5) and continues the execution until the end.
At the moment, our runner expects to receive a Promise. However, by the spec, you can await any value, Promise or not. Fortunately, to solve this is very easy.
Consider the following synchronous add function:
This code crashes our generator, since our generator tries to call a
.then to the yielded value. We can solve this by using
Promise.resolve(arg) copies arg if it is a Promise, otherwise it wraps arg in a Promise. So our runner becomes:
Now our code doesn't crash with non-Promise values:
If we run our code with
addAsync, we will get the same behavior as before!
Since we are using Promises, we can easily get any error/rejection that happens in our asynchronous operations. Whenever a promise rejection occurs, our runner should simply unwrap the rejection reason and give it to the generator to allow for handling. We can do this with the
Now not only we add a
.then, but also a
.catch to the yielded Promise and if a rejection occurs, we throw the reason to
main. Notice that this also handles the case where we are performing a synchronous operation and there is a normal
throw. Since our runner sits below
main in the execution stack, this error will first bubble to the
main and be handled there in the
try...catch. If there was no
try...catch, then it would have bubbled up to the runner and since our runner doesn't have any
try...catch it would bubble up again, the same as in async/await.
We've come a long way. Right now our code is able to deal with one
yield. Our code is already able to run multiple parallel asynchronous operations because we are using Promises, therefore
Promise.all and other methods comes for free. Our runner, however isn't able to run multiple
yield statements. Take the following generator:
Our runner will deal with the first
yield just fine, however it won't correctly give control back to
main at all in the second
yield, the timeout will finish and nothing will happen. We need to add some iteration capability to the runner so that we can correctly process multiple
yield statements. Look at the following code:
We use recursion with an IIFE to iterate through the generator. Instead of directly calling
.next, we recursively call this IIFE with the unwrapped value of the promise. The first thing the function does is to give control back to the generator with the unwrapped value. The cycle then repeats if there is another
yield. Notice that on the last
yield (or if there isn't any), then the generator will end and give control back to the runner. The runner checks if the generator has ended and finishes execution if positive.
There is one problem however: if one of the promises rejects, then the cycle is broken and our runner doesn't run correctly. To fix this we need to add an error flag and call
.throw based on this flag:
We've implemented something really close to async/await. If you look at the V8 blog you will notice that our program does essentially the same thing. I suggest reading the blog post above, there is a cool optimization that if you await promises, then the engine is so optimized that your code will run faster than just using promises with a
For my next post I think I will write about Symbol or the Myers diff algorithm (the default diff algorithm for git). If you have any doubts, sugestions or anything just comment below! Until next time :)