Practical Asynchronous Iteration in JavaScript
By: José Tapadas Alves
Tags: Typescript, JavaScript, Asynchronous, Ecmascript2018, Ecmascript

Recap of Synchronous Iterators and Generators

In this section, we will do a quick review of synchronous iterators and generators in JavaScript so we can more easily extrapolate them into the asynchronous case.

What is an iteration anyway?

Let us look into this question by self-realizing what we usually have at hand when iterating. For example, on one side, we have our arrays, strings, etc. being basically our sources. On the other side, we have what we usually use as our means to consume our data via an iteration — namely our for loops, spread operators, etc.

Based on this, we can look at an iteration as a protocol that, when implemented by our sources, will allow consumers to sequentially “consume” its contents using a set of regular operations. This protocol could then be represented by the following interface:

1.1 Interface for defining a synchronous iterable, iterator, and subsequent result for every iteration.

So, putting it verbosely for those readers who may not be familiar with TS interface descriptions:

A SynchronousIterable provides a method via a Symbol.iterator that would return a SynchronousIterator .

provides a method via a that would return a . Our SynchronousIterator would then return our IteratorResults from its implementation of the .next() method.

would then return our from its implementation of the method. The IteratorResults would then contain a value to hold the current iterated value as well as a done flag that is set to true after the last item is iterated through (and false while iterating).

Note: You can find out more about this by reading the ECMAScript 2021 Language Specification documentation.

An example of using this interface can be easily showcased by manually iterating over an array:

1.2 Simple manual iteration. Notice the fact that “done” is set to true when we are over-transversing the object.

Sources that implement this interface can also be iterated via a for..of iteration directive that you have probably made use of at some point:

1.3 Using for..of to iterate over our source.

Of course, we would expect sources like the built-in array (as used above) to be iterable naturally. To then showcase it differently, we’ll implement that interface, for example, to generate a range of numbers:

1.4 A custom implementation of an iterable source that generates a range of numbers from “start” to “end.”

What about generators?

Usually, functions return either a single value or none. We can think of generators as entities that can return, in sequence, multiple values. To this holding of values, it was attributed the concept of yielding.

1.5 Defining a generator function that yields at the time the same 1, 2, 3 sequence.

These generator functions do not behave as regular functions, as they are lazily evaluated. So when called, they will return you a generator object that will be responsible for managing its execution. These generators are also iterable, meaning they also implement our interface so we can actually loop over them similarly as above:

1.6. Checking the values from our generator function. And yep, the main method of a generator is also .next().

Also, it is also very important to denote that the .next() function is key to obtaining the next yielded value for these generator objects. This will then produce the expected outputs.

As we can now yield values (instead of state), we can then re-implement our iterable range from Figure 1.4, but now using a generator function:

1.7 Our ranged iterable source now implemented using a generator function.

As we can see, we can now leverage a generator function to simplify our original ranged iterable.