Categories


Archives


Recent Posts


Categories


Async Generators and Async Iteration in Node.js

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

This entry is part 4 of 4 in the series Four Steps to Async Iterators. Earlier posts include ES6 Symbols, Javascript Generators, and ES6's Many for Loops and Iterable Objects. This is the most recent post in the series.

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

Series Navigation<< ES6’s Many for Loops and Iterable Objects

Copyright © Alan Storm 1975 – 2020 All Rights Reserved

Originally Posted: 4th August 2020