- ES6 Symbols
- Javascript Generators
- ES6’s Many for Loops and Iterable Objects
- Async Generators and Async Iteration in Node.js
Generator functions predate the introduction of async/await in javascript, which means that while creating an asynchronous generator, (a generator that always returns a Promise
and is await
-able), is possible, it introduces a number of sharp edges and syntax considerations.
Today we’re going to take a look at asynchronous generators and their close cousin, asynchronous iteration.
Important: While these concepts should apply to any javascript that adheres to modern specifications, all of the code in this article was developed and tested against Node.js versions 10, 12, and 14.
Async Generator Functions
Consider this small program
// File: main.js
const createGenerator = function*(){
yield 'a'
yield 'b'
yield 'c'
}
const main = () => {
const generator = createGenerator()
for (const item of generator) {
console.log(item)
}
}
main()
This program defines a generator function, uses that function to create a generator object, and then loops over the generator object in a for ... of
loop. Pretty standard stuff — although you’d never use a generator for something this trivial in real life. If generators and the for ... of
loop are new to you, we talk about them in our Javascript Generators and ES6’s Many for Loops and Iterable Objects articles. You’ll need a solid understanding of generators and for ... of
loops before you take on asynchronous generators.
Lets say we want to use await
in our generator function. Node.js supports this, as long as we declare our function with the async
keyword. If async functions are new to you our Async Javascript series is a good place to learn about asynchronous programming in javascript.
Let’s modify our program to use await
in our generator.
// File: main.js
const createGenerator = async function*(){
yield await new Promise((r) => r('a'))
yield 'b'
yield 'c'
}
const main = () => {
const generator = createGenerator()
for (const item of generator) {
console.log(item)
}
}
main()
Again, in real life you wouldn’t do something like this — you’d likely await
a function/method call from a third party API or library. Our example is kept simple for pedagogical reasons.
If we try to run the above program, we run into a problem
$ node main.js
/Users/alanstorm/Desktop/main.js:9
for (const item of generator) {
^
TypeError: generator is not iterable
Javascript is telling us our generator is not iterable. At first blush, it appears that making a generator function asynchronous also means the generator it produces is not iterable. This is more than a little confusing, as the whole point of generators is to produce objects that are programmatically iterable.
Let’s figure out what the heck is going on.
Examining the Generators
If you’ve read our article on generators, you know that an object is iterable in javascript if it defines a Symbol.iterator
method AND that this method returns an object that implements the iterator protocol. An object implements the the iterator protocol when it has a next
method, and that next
method returns an object with a value
property, a done
property, or both a value
and done
property.
If we compare the generator object returned by an asynchronous generator function vs. a regular generator function using this small test program
// File: test-program.js
const createGenerator = function*(){
yield 'a'
yield 'b'
yield 'c'
}
const createAsyncGenerator = async function*(){
yield await new Promise((r) => r('a'))
yield 'b'
yield 'c'
}
const main = () => {
const generator = createGenerator()
const asyncGenerator = createAsyncGenerator()
console.log('generator:',generator[Symbol.iterator])
console.log('asyncGenerator',asyncGenerator[Symbol.iterator])
}
main()
we see that the former does not have a Symbol.iterator
method, while the later does.
$ node test-program.js
generator: [Function: [Symbol.iterator]]
asyncGenerator undefined
Both generator objects do have a next
method though. If we modify our test program to call this next
method
// File: test-program.js
/* ... */
const main = () => {
const generator = createGenerator()
const asyncGenerator = createAsyncGenerator()
console.log('generator:',generator.next())
console.log('asyncGenerator',asyncGenerator.next())
}
main()
we see another problem
$ node test-program.js
generator: { value: 'a', done: false }
asyncGenerator Promise { <pending> }
For an object to be iterable, the next
method needs to return an object with value
and done
properties. An async
function will always return a Promise
object. This carriers over to generators created with an async function — these async generators always yield
a Promise
object.
This behavior makes it impossible for a generator from an async
function to implement the javascript iteration protocols.
Asynchronous Iteration
Fortunately, there is a solution. If we take a look at the constructor function/class returned by an async
generator
// File: test-program.js
/* ... */
const main = () => {
const generator = createGenerator()
const asyncGenerator = createAsyncGenerator()
console.log('asyncGenerator',asyncGenerator)
}
we see it’s an object whose type/class/constructor-function is an AsyncGenerator
instead of a Generator
asyncGenerator Object [AsyncGenerator] {}
This object may not be iterable, but it is asynchronously iterable.
For an object to be asynchronously iterable, it must implement a Symbol.asyncIterator
method. This method must return an object that implements an asynchronous version of the iterator
protocol. That is, the object must have a next
method that returns a Promise
, and that promise must ultimately resolve to an object with the usual done
and value
properties.
An AsyncGenerator
object meets all those criteria.
All of which leaves one question — how do we iterate over an object that isn’t iterable, but IS asynchronously iterable?
The for await … of Loop
It would be possible to manually iterate over an async-iterable object using just the next
method of a generator. (Notice that our main
function is now async main
— this will let us use await inside the function)
// File: main.js
const createAsyncGenerator = async function*(){
yield await new Promise((r) => r('a'))
yield 'b'
yield 'c'
}
const main = async () => {
const asyncGenerator = createAsyncGenerator()
let result = {done:false}
while(!result.done) {
result = await asyncGenerator.next()
if(result.done) { continue; }
console.log(result.value)
}
}
main()
However, this isn’t the most straightforward looping mechanism. We have the awkwardness of both the while
sentinel and checking result.done
manually. Also, the result
variable needs to live in both the inner and outer block’s scope.
Fortunately, most (all?) javascript implementations that support asynchronous iterators also support the special for await ... of
loop syntax. Try out a main
function that looks like this instead.
const createAsyncGenerator = async function*(){
yield await new Promise((r) => r('a'))
yield 'b'
yield 'c'
}
const main = async () => {
const asyncGenerator = createAsyncGenerator()
for await(const item of asyncGenerator) {
console.log(item)
}
}
main()
If you run the above program, you’ll see our asynchronous generator/iterable object is successfully looped over, and that the loop body receives the fully resolved value of the Promise
.
$ node main.js
a
b
c
This for await ... of
loop prefers an object that implements the async-iterator interface/protocol. You can, however, use it to loop over any iterable object.
for await(const item of [1,2,3]) {
console.log(item)
}
Behind the scenes, when you use for await
Node.js will look for a Symbol.asyncIterator
method on the object first. If it doesn’t find one, it will fall back to using the Symbol.iterator
method.
Non-Linear Code Execution
Just like its sibling, await
, the for await
loop will introduce non-linear code execution into your program. That is — your code will run in a different order than you’ve written it.
When your program first encounters a for await
loop, it will call next
on your object.
That object will yield
a promise, and then execution will leave your async
function and your program will continue to execute outside the function.
Once your promise resolves, execution will return to the loop body with that value.
When the loop finishes and takes its next trip Node.js will call next
on your object. That call will yield another promise, and execution will once again leave your function. This pattern repeats until the promise resolves to an object where done
is true, and then code execution continues after the for await
loop.
You can see this demonstrated by the following example program.
let count = 0
const getCount = () => {
count++
return `${count}. `
}
const createAsyncGenerator = async function*() {
console.log(getCount() + 'entering createAsyncGenerator')
console.log(getCount() + 'about to yield a')
yield await new Promise((r)=>r('a'))
console.log(getCount() + 're-entering createAsyncGenerator')
console.log(getCount() + 'about to yield b')
yield 'b'
console.log(getCount() + 're-entering createAsyncGenerator')
console.log(getCount() + 'about to yield c')
yield 'c'
console.log(getCount() + 're-entering createAsyncGenerator')
console.log(getCount() + 'exiting createAsyncGenerator')
}
const main = async () => {
console.log(getCount() + 'entering main')
const asyncGenerator = createAsyncGenerator()
console.log(getCount() + 'starting for await loop')
for await(const item of asyncGenerator) {
console.log(getCount() + 'entering for await loop')
console.log(getCount() + item)
console.log(getCount() + 'exiting for await loop')
}
console.log(getCount() + 'done with for await loop')
console.log(getCount() + 'leaving main')
}
console.log(getCount() + 'before calling main')
main()
console.log(getCount() + 'after calling main')
This program has numbered logging statements that will let you follow it’s execution. We’ll leave running the program as a exercise for the reader.
While it can create confusing program execution if you’re not aware of how it works, async Iteration is a powerful technique that allows a programer to say
Hey, javascript — don’t block execution of my program to deal with this loop — just do individual iterations of the loop in between other code running in my program