Asynchronous Iterators in JavaScript

Published May 17, 2018
 Asynchronous Iterators in JavaScript

Iterating over a collection of values is a very common operation. And for that, JavaScript provides iteration interfaces such as for loops, map() and filter().

With ECMAScript 2015, the concept of iteration became part of the JS core with the introduction of Iterators and Generators.

Iterators

By definition, an iterator is an object that knows how to access each element at the time.

For that, an iterator provides a method called next(). When called, next() returns the next element in the collection. Mainly, the tuple { value, done }, where:

  • value is the next value in the collection
  • done is a boolean that indicates if the iteration has finished
function myIterator() {
  var array = [1, 2];
  return {
    next: function() {
      if (array.length) {
        return {
          value: array.shift(),
          done: false
        };
      } else {
        return {
          done: true
        };
      }
    }
  };
}

var iterator = myIterator();
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { done: true }

We probably will not need to implement an iterator from scratch. Their creation requires extra care due to the need to explicitly manage the state.

This is where generators come in handy.

giphy.gif

Generators

A generator is a factory function of iterators and a powerful alternative to building an iterator.

It allows us to build an iterator by defining a single function, which maintains the iterator state by itself.

function* myGenerator() {
  var array = [1, 2];

  while (array.length) {
    yield array.shift();
  }
}

var generator = myGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: undefined, done: true }

Iterable

Both Iterators and Generators allow us to build our own iterable objects.

The difference is the explicit (i.e. iterator) versus the implicit (i.e. generator) management of state.

An object is iterable if it defines its iteration behavior. Meaning that an object needs to have the property [Symbol.iterator] defined and assigned to the iteration behavior.

An iterable has the ability to iterate over its values through a for..of loop.

for..of

A for..of statement creates a loop capable of iterating over iterable objects. The statement starts by invoking the custom [Symbol.iterator]() method on the collection, which returns a new iterator object.

The loop iterates by calling next() method on the iterator object. This method is called until the iterator returns the object { done: true }.

var iterable = {
  [Symbol.iterator]: myGenerator
};

for (let item of iterable) {
  console.log(item);
  // 1 2
}

However, the current JavaScript Iterator and Generator only works for synchronous data sources.

We can iterate over an asynchronous data source, but the iteration would complete before all values have been resolved.

var iterable = {
  [Symbol.iterator]: function* generatorWithPromise() {
    // define an array of async data
    const promises = [Promise.resolve(1), Promise.resolve(2)];

    while (promises.length) {
      yield promises.shift();
    }
  }
};

for (item of iterable) {
  item.then(console.log);
}
console.log("done");
// done <- it should be the last to be printed
// 1
// 2

This is because the Iterator is a sequential data operator. It enables iteratation over a collection in a synchronous way.

The interface of next() returns the tuple { value, done } and the values of value and done need to be known at the time the iterator returns. Therefore, an iterator is most suitable for synchronous data sources.

An iterator does not work with any asynchronous data sources.

And an iterator of promises is not sufficient. This will allow the value to be asynchronous but not the value of done.

We can make value async but not status.

This is where async iterators come in.

giphy.gif

But before that, a quick recap.

Async Function

An async function contains a piece with an await expression. The await expression pauses the execution of the function until the passed promise resolves. Once resolved, the function resumes its execution.

(async function async() {
  var one = await Promise.resolve(1);
  var two = await Promise.resolve(2);
  console.log(one, two); // 1 2
})();

Back to async iterators.

Async Iterators

Async iterators are like iterators, but this time, next() returns a promise. This promise resolves with the tuple { value, done }.

A promise needs to be returned because, at the time, the iterator returns the values of value and done are unknown.

function asyncIterator() {
  const array = [1, 2];
  return {
    next: function() {
      if (array.length) {
        return Promise.resolve({
          value: array.shift(),
          done: false
        });
      } else {
        return Promise.resolve({
          done: true
        });
      }
    }
  };
}

var iterator = asyncIterator();

(async function() {
    await iterator.next().then(console.log); // { value: 1, done: false }
    await iterator.next().then(console.log); // { value: 2, done: false }
    await iterator.next().then(console.log); // { done: true }
})();

As iterators introduced [Symbol.iterator] to obtain the iterator from an object, async iterators introduce [Symbol.asyncIterator]. This allows us to customize an object as an async iterator.

The concept of an async iterator is the concept of a request queue. Because an iterator method can be called before the previous requests have been resolved, it needs to be queued internally.

for-wait-of

With async iterators comes the statement for-wait-of, which iterates over an async data source.

var asyncIterable = {
  [Symbol.asyncIterator]: asyncIterator
};

(async function() {
  for await (const item of asyncIterable) {
    console.log(item);
    // 1 2
  }
})();

As for iterators, the for-wait-of loop starts by creating the data source through [Symbol.asyncIterator](). For each time next() is called, the for-wait-of implicitly await for the promise to resolve. This promise is returned by the iterator method.

Async Generators

Async generator returns a promise with the tuple { value, done } instead of the directly returning { value, done }. This allows the generator to work over an asynchronous data source.

await expressions and for-wait-of are allowed for async generators.

The yield* statement supports delegation to async iterables.

var asyncIterable = {
  [Symbol.asyncIterator]: async function* asyncGenerator() {
    var array = [Promise.resolve(1), Promise.resolve(2)];

    while (array.length) {
      // it waits for the promise to resolve
      // before yield the value
      yield await array.shift();
    }
  }
};

(async function() {
  // it waits for each item to resolve
  // before moving to the next()
  for await (const item of asyncIterable) {
    console.log(item);
    // 1 2
  }
})();

giphy.gif

Conclusion

The iterator interface brought by ECMAScript 2015 is designed to iterate over sequential data sources.

An iterator object has the property next() with returns properties { value, done }. The property value contains the next value in the collection. As for done, that contains the boolean value indicating whether the iteration has ended or not.

Since both values value and done need to be known at the time the iterator method returns, iterators are only suitable to iterator over synchronous data sources.

However, many data sources are asynchronous. Examples are I/O access and fetch. To these asynchronous data sources, iterator are not applicable.

For this, JavaScript introduces the AsyncIterator interface.

AsyncIterator is much like an iterator, except that the next() property returns a promise with the tuple { value, done } instead of the direct value of { value, done }.

const { value, done } = syncIterator.next();

asyncIterator.next().then(({ value, done }) => /* ... */);

In order to allow to build a customize asyncIterable, it introduces the new symbol Symbol.asyncIterator. An object can become an asyncIterable by adding this property and implementing its async iterator behavior.

const asyncIterable = {
    [Symbol.asyncIterator]: /* asyncIterator */
};

The asyncIterable introduce a variation of for-of iteration statement, mainly for-wait-of. This statement is able to iterate over async iterable objects.

for await (const line of readLines(filePath)) {
  console.log(line);
}

Async generator function allows us to iterate over an async data sources without worry about managing the iterator state.

As for async iterators, it returns a promise with the value { value, done } and await expression and for-await-of statements are allowed. The yield statement support delegation to async iterables.

Thanks to 🍻

Discover and read more posts from Tiago Lopes Ferreira
get started