JavaScript Async Iterators and Generators

Asynchronous iterators allow iterating over data, which comes asynchronously. For example, downloading something chunk-by-chunk over the network.

With asynchronous generators, it becomes more convenient. To be more precise, let’s start at grasping the syntax.

Async Iterators

Async iterators are, generally, like regular iterators. But, there are several syntactical differences.

A regular iterable object is the following:

let range = {
  from: 1,
  to: 7,
  // for..of calls this method once at the very beginning
  [Symbol.iterator]() {
    // ...returns the iterator object:
    // go ahead, for..of works only with this object,
    // request  for next values using next()
    return {
      current: this.from,
      last: this.to,
      // next() is called on each iteration in the for..of loop
      next() { // (2)
        // it return the value as an object {done:.., value :...}
        if (this.current <= this.last) {
          return {
            done: false,
            value: this.current++
          };
        } else {
          return {
            done: true
          };
        }
      }
    };
  }
};
for (let value of range) {
  console.log(value); // 1 then 2, then 3, then 4, then 5, then 6, then 7
}

For making the object asynchronously iterable, it is necessary to act in this way:

  1. Using Symbol.asyncIterator rather than Symbol.iterator.
  2. A promise should be returned by the next().
  3. It is necessary to use the for await (let item of iterable) loop for iterating over an object like that.

Let’s check out an example:

let range = {
  from: 1,
  to: 7,
  // for await..of calls this method once at the very beginning
  [Symbol.asyncIterator]() {
    // ...returns the iterator object:
    // go ahead, for await..of works only with this object,
    // request it for next values using next()
    return {
      current: this.from,
      last: this.to,
      // next() is called on each iteration in the for await..of loop
      async next() {
        // it return the value as an object {done:.., value :...}
        // (automatically wrapped in an async promise)
        // inside you can use await, do async things:
        await new Promise(resolve => setTimeout(resolve, 1000)); 
        if (this.current <= this.last) {
          return {
            done: false,
            value: this.current++
          };
        } else {
          return {
            done: true
          };
        }
      }
    };
  }
};
(async() => {
  for await (let val of range) { 
    console.log(val); // 1,2,3,4,5,6,7
  }
})()

Async Generators

JavaScript also supports generators. They are also considered iterable.

Let’s consider a regular generator that creates an order of values from the start to the end, like this:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}
for (let value of generateSequence(1, 7)) {
  console.log(value); // 1, then 2, then 3, then 4, then 5, then 6, then 7
}

So, await can be used in regular generators. There is no space for a delay in the for..of loop, as all the values should come asynchronously.

For using await in the generator body, you can prepend it with async in this way:

async function* generateSequence(start, end) {
    for (let i = start; i <= end; i++) {
      await new Promise(resolve => setTimeout(resolve, 1500));
      yield i;
    }
  }
  (async() => {
    let generator = generateSequence(1, 3);
    for await (let val of generator) {
      console.log(val); // 1, then 2, then 3
    }
  })();

There appeared an async generator, which is iterable with for await...of

Another difference of the asynchronous generator is that the generator.next() method is asynchronous, too and returns promises.

So, in a regular generator,result = generator.next() is applied for getting values. In the async generator, await is added in this way:

results = await generator.next(); // results = {value: ..., done: true/false}

Async Iterables

To transform an object into an iterable, Symbol.iterator should be added to it, like this:

let range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    return <object with next to make the range iterable>

  }
}

A frequently used practice for the Symbol.iterator is returning a generator, instead of a plain object using next.

Let’s see an example:

let range = {
  from: 1,
  to: 3,
  * [Symbol.iterator]() { // shorthand for [Symbol.iterator]: function*()
    for (let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};
for (let val of range) {
  console.log(val); // 1, then 2, then 3
}

The custom object range is considered iterable. The *[Symbol.iterator] generator performs the logic for the listing values. Here is an example:

let range = {
  from: 1,
  to: 3,
  async * [Symbol.asyncIterator]() { // same [Symbol.asyncIterator]: async function*()
    for (let value = this.from; value <= this.to; value++) {
      //  pause between values, wait for something
      await new Promise(resolve => setTimeout(resolve, 1500));
      yield value;
    }
  }
};
(async() => {
  for await (let val of range) {
    console.log(val); // 1, then 2, then 3
  }
})();

The values now will come with a delay of a second between them.

Summary

Regular generators and iterators work perfectly with the data that takes no time for generating.

While waiting for the data to come asynchronously with delays. Here for await..of is used rather than for..of.

Also, async generators can be used for processing streams of data that flow chunk-by-chunk.




Do you find this helpful?

Related articles