Implement the Async/Await Pattern and be Familiar with the Generator
Understanding the main ideas of this pattern by implementing it from scratch (in an evolutionary way)
If you came here and want to know more about async/await
pattern, you probably have some experiences of using callbacks and had some struggles with callback hell
(if you need to review some of the quirks about callbacks, you should check this article out). Also, you might have some experiences of working with promise
pattern.
Prerequisites for going further in this article is to have some ideas of how to use promise
; but if you don’t, you’d better visit this page on MDN before continuing (and if you are really curious about the promise
pattern and intend to fully understand it, you can read the book that I’ve published here for free).
Start from the past
Let’s back to the days that there wasn’t any syntax for using async/await
pattern to make the workflow of our codes clean and beautiful.
In those days, after adding promise
natively in ES6 (aka ECMAScript 2015) and hanging around with this new pattern, we found that this pattern is really useful. It can solve many of the problems and struggles that we had with the ugly callback pattern and also makes our codes more readable.
But as we all agree, nothing is perfect and everything can improve
When we had started using promise
instead of callbacks wherever possible, we saw that we still have to repeat the then
method to run some sequences. There is still some boilerplate that is cumbersome to use.
On the other hand, it was kind of pain that we couldn’t access the variables in the previous then
method scope unless we defined them globally (which is not a good idea).
And some of these sorts of excuses had bothered us.
Make a long store short, finally, we realized that we need something that makes our asynchronous workflow to look more synchronous. Because our mind is used to understand that structure better and feels more comfortable with that.
So to meet these needs, we started to analyze some ideas that came to our mind, and all of a sudden, we noticed that another cool and magical feature was added to JavaScript in ES6 as well.
And that was the generator
function.
Let me introduce you to this unicorn
The generator
function has a weird and amazing structure somehow. It is the only function in JavaScript that its execution can be paused at a point and then be resumed again (actually I fell in love with it).
Note: Here we are not going to deep dive into the
generator
function. We just want to get familiar with it to see some of its practical benefits that can help us to implement theasync/await
pattern.
Let’s see its applications in an example:
As you can see, there are only two things in its syntax that we must mention:
- The asterisk (
*
) before the name of the function, defines that this function is agenerator
function. The asterisk just must be put between thefunction
keyword and the name of the function (which isgen
here). So all of these states are correct:function* gen
,function *gen
,function * gen
,function*gen
. yield
keyword that is the point that function will be paused there.
The behavior of the generator
function is different from other functions (make yourself ready to see some magic).
When you invoke this function by using gen()
, the codes inside the function wouldn’t be executed immediately. By invoking that, it just returns an object (which is called a generator
object) that has some methods on it. One of its most important methods is the next
method.
At this point, by calling this next
method we can run the gen
function until we reach the first yield
keyword and it would be paused there automatically.
When the execution be paused at that point, the value in front of the yield
would be returned and it’s accessible by the next
method.
As you can see, the returned value of the next
method is an object which has simply two properties.
- Value: It is the value in the front of
yield
that be returned on each pause - Done: It shows whether the execution of the
gen
function is done
After you call the next
method a few more times, you will get the below result:
After the third time that the next
method be invoked — because there isn’t any yield
keyword anymore — the execution of the gen
function be done (which you can see in the done
property) and also the value in the value
property becomes undefined
.
Actually, the last value is fed by the return
statement, and because we don’t have any return
keyword here, it is undefined
(if a function doesn’t have any return
keyword, JavaScript returns an undefined
value from that function by default).
So far, we learned that a generator
function can be paused at the position of a yield
keyword and also returns the value that is in the front of the yield
on each pause.
Another amazing thing that we can do is to pass an argument on each pause.
There is a point here that is important to keep in your mind; the argument of the first next
method will be thrown away and by invoking that you are just able to get the returned value of the yield
statement (I know it may sound weird at first, but as thinking about it more, it will make sense eventually).
But by invoking the second next
method you can pass an argument to the first yield
and execute the rest of the code until reaching the next yield
statement (or a return
keyword, or the end of the function).
As you can see here we used a return
statement in the gen
function, then we got the returned value on calling the last next
method.
The only two other things and we might need to know about the generator
for this topic, are the return
method and the throw
method on the generator
object.
Whenever these both methods be called — that doesn’t matter at each pause — the execution of the generator
function would be stopped immediately no matter what.
The only difference between them is that when we use the return
method, we just end the function normally and get an object like this { value: undefined, done: true }
(the value
property equals to the argument of the return
method). But when we call the throw
method, we end the function by throwing an exception.
Let’s back to asynchronous stuff
So far we have learned about some of the magical applications of the generator
function. What if we use the generator
for controlling the workflow of our asynchronous functions?
The idea is that we can use the generator
to run the async functions and then pause until that function be resolved. The important thing about pausing with the generator
is that it doesn’t block the program. Then it can be resumed again by the async function itself.
Let’s see the idea in the code to understand it better:
Until here, we called an asynchronous function in the front of the yield
that returns a promise object. It means the returned value of calling the next
method would be a promise object (unlike the previous examples that we returned just a simple value).
After this point, we can call the then
method on the promiseObject
because it’s just a normal promise that we used already.
The key point here is that we called the next
method inside the then
method (which means after resolving the async function) and pass the resolved data to it as an argument.
This data will go back to the gen
function and will be put inside the fetchedResult
variable. Also the execution of the gen
function will be resumed.
This code is kind of ugly, I know, but if you just take a closer look at the gen
function, you can notice that we call an asynchronous function in a way that the workflow looks like synchronously somehow (of course it works asynchronously but the appearance of it is similar to synchronous codes).
I guess we got the idea at this point (if you didn’t, just try to play with the code or continue reading the article to eventually grasp it).
Before going further, let’s refactor this code to be more comfortable for adding features and some improvements.
This is exactly the previous code that we simply wrapped all the operations inside a function named asyncController
and passed the generator
function to it as an argument.
Add features and improvements
The first thing that we notice which needs improvement is that our asyncController
function now can handle just one single async function and one pause. How about running a bunch of asynchronous functions one after each other?
We need some improvements in our asyncController
to be able to handle a situation like below:
All the operations (i.e. fetchData
, fetchedResult.json
, saveData
etc.) are asynchronous and we want them to be run respectively.
In order for the asyncController
to handle this kind of situation, it just needs to repeat its operation until the last yield
statement be resumed and the generator
function is done.
But for implementing this iteration we cannot just use a simple loop (i.e. for(){}
, while(){}
, do{}while()
etc.), because these sort of iterations are unpausable. We need a solution that we could control the flow of the iteration.
A way that comes to our mind is to use another function to run the operation and call it again whenever we want to repeat the operation for another yield
statement.
Except for the first line that we just get the generator
object from the generator
function, we put the rest of the codes inside another function named iterator
, and call it for the first time.
Look inside the then
method of the promiseObject
— I just commented the previous code for you to see the change — we didn’t call the next
method here, rather we called the iterator
function recursively and send the data to it as an argument. The argument will be passed to the next
method on the next round and this pattern will be repeated.
But wait! We forgot something here. How long is it going to iterate? Where is the base case of this recursive function?
We remember that the returned value of the next
method is always an object which has two properties and one of them is the done
property that tells us whether the generator
function has been done.
Here we can use done
property for the base case to terminate the iterator
function.
OK! This improvement is done! Let’s go to the next one.
The next thing that we ought to mention is that we all know whenever we use promise, it is wise to always add a catch
method at the end of the chain.
So what do we suppose to do with the error that we catch here?
As we said before, the generator
objects have another method named throw
that is the best time to use it. We can throw the error to that method which would be handled inside the generator
function.
You can call the throw
method inside the catch
method but in my opinion it’s better not to do that and call the iterator
function again and then send the error to it. Because I think we should put all operations related to the generator
object in one place (if you don’t like this approach, you can do that in your way. There might be plenty of ways to do that).
But if we send the data as well as the error in the same argument, how can the generator
object recognize to run which method of itself?
Because for the errors it must call its throw
method and then call its next
method for the correct data.
Also, there are many ways to do that which is up to you to decide. Here we choose to send the name of the method that must be called, as another argument (which may not be the best way).
Look at the code carefully. As you can see we pass the name of the method alongside the value (which can be the correct data or an error). Also we need to pass the next
method name for the first time that we called the iterator
function.
So now we pass the error to the generator
function. But how is it going to be handled there?
Because the generator
function runs synchronously, the errors that might occur there can be handled in a synchronous approach as well. Yep, I’m talking about try/catch
blocks.
Oh! Look at this. It looks beautiful, doesn’t it?
But we’re not done yet, there are still some things we need to watch out. Let’s back to work.
By our implementation so far, the asyncController
only enables us to handle async functions. But how about handling a simple value? What if someone put a string value in front of a yield
statement?
Because we assume that everything is put in the front of a yield
should return a promise object, it’s a good idea that we wrap any kind of value inside a promise before resolving.
By using the resolve
static function on the Promise
constructor, we can wrap any kind of value inside a promise object.
The next important thing which must be considered is that it is expected that our asyncController
function should return a new promise object.
It makes our controller handier to be used with other workflows and the result of that be handled by another function. So returning a new promise object is a wise choice.
In order to return a promise object, you can use the resolve
static function that we used before, or simply use the new
keyword before the Promise
constructor to create a new object and return that.
Let’s use the new
keyword this time.
First let’s wrap everything inside the callback of the Promise
constructor.
Then the resolve
and the reject
functions should be used somewhere.
The resolve
function must be called here because this line is the last thing that will be run if everything goes well.
And the reject
function must be called whenever that something goes wrong inside the generator
function.
The only place that we can catch its error is when we call the methods on the generator
object.
Let’s put it inside a try/catch
and call the reject
function in the catch
block.
By returning a promise object, you are also able to add a then
and a catch
method after the asyncController
function, or maybe use that inside another generator
function which is wrapped by an asyncController
.
We are done. This is kind of the implementation of the async/await
pattern, that we just don’t have its syntax.
Note: you can get the source code from here which is also a much cleaner version of it.
Conclusion
In this article we just focused on the understanding and implementation of the main concepts of the async/await
pattern. definitely, this is not the exact implementation that is used to implement this pattern natively.
Here we intended to just talk about the ideas and concepts of this pattern and also get you familiar with the generator
functions.
Now that you saw the underneath of these structures, maybe some thoughts might fly to your mind that could change everything and build some new patterns and structures which be used for some special cases.
Also fully understanding this kind of stuff can help you to understand the more complicated patterns.
As a programmer, do not distract your mind by some fancy names and tools. Your mission is to swim deep in the oceans of the programming concepts by your mind.