Some exciting features Javascript has developed during the latest versions (ES6, ES2016) are without doubt the generator functions and the async/await support.
These features exist in Typescript for quite some time; the language supported async/await since 1.7 version. But what about them? What they really are and how they work?
In this post I am going to explore their world, their Typescript syntax and implementations.
What is a generator
A generator is a special type of function that during its execution it can be paused, yield results back the caller and resume later, at caller's convenience. And this can happen as long as the generator has something to return, some value to yield back.
This is a very powerful feature, as you can control the flow, send messages back and forth; the generator sends the caller a message every time it yields and the caller passes a message for the generator, when it is restarting it. As we can see later, this kind of behavior is ideal for state machine like style of code, which in this case is the async/await style.
How it works
First, I will talk a little bit about ES6 and how it works there and then I will go to Typescript, but the syntax does not differ.
So, in order to create a generator you use an asterisk after the function keyword and then you casually declare a function, just like this:
Congratulations, you have made a generator!
Of course, there is more on this. In order for the generator to be useful, you need to use the yield expression, to delegate result and control to the caller object.
Note that when you call a generator, you casually call a function, but it's body will not execute, not until you request the generator iterator to iterate to next item.
Say what???
I may have gone a bit ahead of myself here. Let me explain.
Yield expression
With this expression you can send value back to the caller. You use the yield
keyword. This creates a lazy iterator. The following is a very simple generator
The yield expression is the code on line 2, the yield "Yo!"
. You can yield whatever you want, you might want to yield a number, an object, callback or a promise, it is up to you. Generator's job is to return this to the caller once it reaches to that point in the function body.
Generator iterator
Function myGenerator
is pretty useless if I don't execute it. Calling this method, I will create a generator iterator. Under the hood, generators support iterators in order to move to the next value. So, the following example, will create that generator iterator object and will iterate that. If I did it right, I will see the "Yo!" value.
Check the following plunk to interact with the code a bit.
On line 5, I create the iterator. Nothing happens.
When I call the .next()
method, generator will execute, reach the yield
, which in turn sends an object to the caller. Generators yield objects with a value and a done property.
- value property: Contains whatever value you pass back with the yield expression
- done property: A boolean that indicates if iterator should finish execution or not.
In the example above, you see, when I fist call .next()
, I get { value: "Yo!", done: false }
, which, okay has the value, but why generator hasn't finished?
There is no other code to execute after the yield
expression.
Yes there is not, but generator is not aware at the moment, you need to resume his execution in order to let him know that his job is done.
When it reaches beyond the last yield, it returns an undefined
value and the done
property is true.
Check the next example and notice three things that happen during the runtime:
See what I see?
- The function body does not execute not until I call
.next()
- The function pauses its loop that is running to return the result. In the meantime I can do whatever I want and call it whenever I want. Generator will patiently wait for me to delegate control back to it
- When I call the
.next()
method, only and only then the function resumes (the loop in this example)
Of course, it exits when there is no yield expression to execute. Technically, I does not exit, the iterator is just finished iterating through.
Dancing back and forth
I mentioned earlier that generators allow you to do that dance, from the generator function to the caller and from the caller back to the function. You just need to pass a value to the .next()
method and it will evaluate from the yield expression.
In the next example, I create a generator that accepts a string input, yields it and when it resumes it console.log
's the concatenated values (the initial when generator iterator was created and the one that is passed from the .next()
method)
It's a bit weird. What is going on? Shouldn't I pass the value in the first time I call the .next()
method?
Nope. When you call the .next()
for first time it reaches till yield
. There, it returns the hello
variable. Only that. I hasn't yet returned a value to the world
variable! The external system correctly sees the "Hello" value being returned in first iteration, this is yielded by the generator.
In second iteration, I pass the "World" string. This is what is assigned to the world
variable, by evaluating the yield hello
expression! Code executes until the console.log
statement, where I concatenate the hello
and world
variables. From that point on, the generator knows that this is the end and returns an undefined value, indicating the iteration is over.
Check this one to understand when the yield expression is returning a value and when it is evaluated
The generator function receives a parameter.
On first iteration, the yield value
executes, the one in the parenthesis and returns to the external system. No concatenation has happened yet.
In second iteration, I pass the string "George". The (yield value)
expression returns that string in the function, which is then concatenated with the rest, resulting in the printed value.
Generators and promises
In this example I am going to demonstrate that two-way communication between the generator and the external system.
In the following code, I create a function which whenever is called, it returns a promise. That promise generates a random number between 0 and 20 each time and it resolves if it is greater than ten. If less or equal to ten it rejects the promise.
Then I created a wrapper for the generator that I am going to write. That wrapper accepts a generator function as an argument, it creates a generator iterator and calls .next() to fetch generator's value, which happens to be a promise. I use the .then()
and .catch()
methods to send a value or an error object back to the generator.
Finally, I create the generator function itself, which yields the promise I made earlier. If it is resolved, it takes its value and copies it to an element's innerHTML
. If an error object is returned (from the wrapper when the promise is rejected), then the catch
body will execute, printing the error's message.
The makeRandom()
method is just a helper. I also registered an event listener to a button to repeat the process.
And here is a plunk to play with the source code.
As you can see, this one here, has a lot of similarities with async/await. Imagine, instead of yield
was the await
keyword. The code would look the same, wouldn't it? More on this in next sections.
Typescript. What does it generate?
From this:
It generates this:
Nice, let's get a little bit ahead and compare this with the code that is generated for the async/await
From this:
It generates this:
Well, comparing the output, I can see similarities. Both produce a generator. So, we actually see that async/await
is based on generators for its functionality. Let's explore the reason in the next section.
Async/Await
Let's take a step back and think for a moment. When you write an async function, with an awaiter, what is yourexpectation?
You expect it to pause the execution when the execution reaches the await
keyword in front of a method. The execution is back to the caller once the method that is awaited is finished.
You also expect in some cases to pass a value to the method you await.
Lastly, you might expect it to throw an error.
Based on our analysis in this post, these three attributes are supported by generators. Thus, they are perfect for implementing the async/await
feature.
Also, if you look back on the section Generators and promises, you will see that this code resembles an async/await pattern a lot. What I did there was to pause the execution and pass control (the promise) to the caller and the caller tried to resolve the promise, pushing a value back to the generator (from the .then()
or .catch()
methods).
The generator could also handle exceptions (rejections on the promise) gracefully.
Syntax
You prefix the function with the async
keyword and inside the function body you prefix with the await
keyword the promise method you are about to call.
If you mark a function as async, it always returns a Promise. So, if you do not return something from the function body, your function is of type Promise<void>
.
Let's look at some more examples. I am going to demonstrate usage of async
functions that do not return a value (return a Promise<void>
), functions that return some kind of value and functions that fail, because of Promise
rejection.
Void functions
The following function is printing into the console each name in the array, it does not return anything though.
Another example. The following function is updating an HTML element with a greeting, if it is morning, it prints "Good morning", for afternoon it prints "Good afternoon" and for evening "Good evening" along with the current time and day. The setTimeout
is there to simulate a delay.
Notice in all the examples, I wrap the Promise
in a function, which returns it.
Returning a value
Next example. In this one, I am going to return an object, so I need to resolve the promise that is returned by the async
function.
The object I am returning is of type Person
, which I created, in line 1, just a simple object with a string
and number
properties. I return this from the Promise
, passing the object into the resolve
method. Again, setTimeout
is simulating a delay. After I fetch the return value, I create some appropriate messages to show on the UI.
Please note that I am using .then()
/.catch()
only because I am not in an async
function, if I were, I could just await that .fetchPerson()
method, like this
Rejecting the promise
In this last example, I am going to reject a Promise
and show the error message on screen. When you are rejecting a Promise
, the await will throw an error, so it's a good practice to wrap those await
calls around a try/catch
block. Also, the value that is passed into the reject
function of the Promise
, is going to be the error object in the catch
block.
If you wish to fiddle around with those, I have a plunk ready, give it a go, get familiar with the syntax.
Summary
Async/await is build on generators. Generators make us able to lazy execute code, enabling a two-way communication between an external system and the generator function itself.
Generators in Typescript have same syntax as in ES6, so async/await, but Typescript provides us with strongly typed methods, so we can use the API at its best.
A generator's body is not called, not until the .next()
method from the generator iterator is called. This executes the yield expression inside the generator, until there is no yield
to call, in this case terminating the iterator.
A generator sends a message/value to the caller and it can also receive input from the caller, through the .next()
method, which is evaluated as a return value from the yield expression.
Async/await is available in Typescript from 1.7 version and onward, but only on ES6 targets. From 2.1 though it is available on ES3 and ES5 targets as well.
In order to use async/await, prefix your function with the keyword async
first and you can call a promise inside its body, by prefixing it with the await
keyword.
An async
function always returns a Promise.
Make sure you wrap the Promise object around a function.
Comments powered by Disqus.