Getting Started with Asynchronous Iterators and Generators

Chris Nwamba
👁️ 2,533 views
💬 comments

Introduction

It’s been a long while coming and I feel it’s high time I made a post about it. Asynchronous Iteration could become the next big thing in JavaScript. With the proposal to include them up all the way to stage 3, I think it’s only right to learn about it and you should too.

In this post, we'll discuss asynchronous iterators as well as generators, where they originated from (there’s always a beginning), what they are used for and how they can make our lives (not just our code) better.

One person who inspired this post is Domenic Denicola, his efforts on the Chrome team and the community as a whole have been astounding. Okay, let’s get digging!

TLDR

I bet you didn't know that the following code block is a thing:

for await (const info of getApi(apis)) {
 console.log(info);
}

Yes, it is and as a matter of fact now has a good wide browser support. The code sample iterates over an async operation, getApi, without the need to wait for the operation to resolve.

Asynchronous Behavior

Moving on to iterators without first having a recap on how async work in JS could be a bit misleading. Basically when you execute an operation asynchronously, you have room to move on to another task before it finishes executing and that’s exactly what asynchronous functions are. Below is a setTimeout function, the most basic form of asynchronous JavaScript ever written:

  // Say "I have Mac and Cheese"
  console.log("I have Mac and Cheese");
  // Say "Sure thank you" three seconds from now. 
  setTimeout(function() {
    console.log("Sure thank you");
  }, 3000); 
  // Say "Want Some?"
  console.log("Want some?");

  //console logs:
  > I have Mac and Cheese
  > Want some?
  > Sure thank you

Synchronous Iteration

Iteration is basically a way of traversing data. It works via a number of concepts which are:

Iterables: These are data structures that can be iterated. They show they can be iterated by using the Symbol.iterator protocol. This protocol defines that an object should exhibit iteration behavior.

Iterator: This is an object that is returned by invoking [Symbol.iterator]() on an iterable. Using its next() method, it wraps around each iterated element in the data structure and returns it one by one.

IteratorResult: A new data structure returned by next()

Examples of data structures that can be iterables include arrays, strings and maps. The code block below demonstrates how synchronous iteration works:

  const iterable = ['x', 'y', 'z'];
  const iterator = iterable[Symbol.iterator]();
  iterator.next() {
    value: 'x',
    done: false
  }
  iterator.next() {
    value: 'y',
    done: false
  }
  iterator.next() {
    value: 'z',
    done: false
  }
  iterator.next() {
    value: undefined, 
    done: true
  }

In the example above, the property value has an iterated element, next() keeps on returning each element in the data structure until it is finished thus the value undefined. For every valid value returned, property done is true, after the last element it reverts to false.

Let’s take a more detailed example. Here Symbol.Iterator is used once more to iterate through the letter array in the iterable codebeast:

  const codebeast = {
    [Symbol.iterator]: () => {
      const letter = [`c`, `o`, `d`, `e`, `b`, `e`, `a`, `s`, `t`]; 
      return {
        next: () => ({
          done: items.length === 0,
          value: items.shift()
        })
      }
    }
  }

We can iterate through the codebeast object using a for... of loop:

  for (const letter of codebeast) {
    console.log(letter)
    // <- `c`
    // <- `o` 
    // <- `d` 
    // <- `e`
    // <- `b`
    // <- `e`
    // <- `a`
    // <- `s`
    // <- `t` 
  }

Asynchronous Iteration

Asynchronous iteration works almost like synchronous iteration except that it involves promises. The need for iterating through asynchronous data sources brought about asynchronous iteration. In the code block below, readMemo() cannot return its data asynchronously:

  for (const word of ourMemo(memoName)) {
    console.log(word);
  }

With asynchronous iteration, the concepts Iterable, Iterator and IteratorResult work a bit differently. In asynchronous iteration, iterables use the method Symbol.asyncIterator. Instead of returning Iterator results directly, the next() method of an async iterator returns a promise. In the code clock below let’s try to make the codebeast iterable asynchronous:

  const codebeast = {
    [Symbol.asyncIterator]: () => {
      const letter = [`c`, `o`, `d`, `e`, `b`, `e`, `a`, `s`, `t`];
      return {
        next: () => Promise.resolve({
          done: letter.length === 0,
          value: letter.shift()
        })
      }
    }
  }

We can carry out our iteration asynchronously using a for... await... of loop:

  for (const letter of codebeast) {
    console.log(letter)
    // <- `c` 
    // <- `o`
    // <- `d`
    // <- `e`
    // <- `b`
    // <- `e`
    // <- `a`
    // <- `s`
    // <- `t`
  }

Another way of showing asynchronous iteration is a function that can fetch a series of web APIs in succession:

  const displayApi = server => ({
    [Symbol.asyncIterator]: () => ({
      x: 0,
      next() {
        if (server.length <= this.x) {
          return Promise.resolve({
            done: true
          })
        }
        return fetch(server[this.x++])
          .then(response => response.json())
          .then(value => ({
            value,
            done: false
          }))
      }
    })
  })

  const apis = [
    `/api/random-names`,
    `/api/random-beer`,
    `/api/random-flag`,
    `/api/random-book`
  ];

  for await (const info of getApi(apis)) {
    console.log(info);
  }

Notice how the code looks synchronous yet will operate asynchronously? Using for...await...of in asynchronous iteration is awesome because it delivers each value once asyncIterator.next() is resolved, it also ensures that asyncIterator.next() is not called for the next item in the array until the current iteration is finished. This enables your responses not to overlap thus they will be returned in the correct manner.

Generators

Another exciting feature introduced in ES6, generators are mainly used to represent sequences that could possibly be infinite. Generators can also be though of as processes that you can resume and pause. The syntax of generators is thus:

  function* genFunc() {
    console.log('One');
    yield;
    console.log('Two');
  }

Where function* is a new keyword used for generator functions, yield is an operator which a generator can use to pause itself and also use to receive input and send output. Enough said, let’s proceed to work with generators. The code block below uses a generator to return positive integers:

  function* countUp() { 
    for (var i = 0; true; i++) { 
      yield i
    }
  }
  for (var i of countUp()) {
    console.log(i)
  }

In the example above, the countUp function is lazily evaluated, it pauses at each yield and waits till another value is asked for. This means the the for... of loop will keep on executing since our integer list is infinite. This brings a host of possibilities that can help us implement asynchronous operations such as loops and conditionals in our functions. Outrightly, generators don’t have a way of representing results of asynchronous operations. To do that, they have to work with promises. Speaking of promises, let’s see how we can iterate through a generator function using the .next method:

  function* race() {
    var lap1 = yield 20; 
    assert(lap1 === 35); 
    return 55;
  }

  var r = race();
  var lap2 = r.next();
  // => {value: 20, done: false}
  var lap3 = r.next(35); 
  // => {value: 55, done: true} 
  //if we call r.next() again it will throw an error

In the example above, r.next() is called once to get it to the yield and then called a second time and passed a value which is the result of the yield expression. This way, race() can then proceed to the return statement to return a final result. This can be implemented by calling .next(result) to show that a promise has been fulfilled with result.

But what if our promise that is yielded is rejected? We can show this by using the .throw(error) method:

  var shortcut = new Error('too fast'); 
  function* race() {
    try { 
      yield 100;
    } catch (h) {
      assert(h === shortcut);
    }
  }
  var r = race();
  r.next();
  // => {value: 100, done: false}
  d.throw(shortcut);

Just as in the previous example, r.next() is called to obtain the first yield keyword.We use r.throw(error) to signal rejection as it causes our generator to behave like an error was thrown by yield. This automatically triggers the catch block.

Let’s take one more example where we attempt two-way communication with generators. Here next() can actually assign values which are received from the generator:

  function* techInterview() { 
    const answer = yield 'Who is the CEO of Tesla?';
    console.log(answer);
    if (answer !== 'Elon Musk') 
    return 'No Way!'
    return 'Okay, on to the next question';
  }

  { 
    const Iterator = techInterview();
    const q = Iterator.next() .value; // Iterator yields question
    console.log(q); 
    const a = Iterator.next('Scott Hanselmann') .value;
    // Pass wrong answer back into generator
    console.log(a);
  } 
    // Who is the CEO of Tesla? 
    // Scott Hanselmann
    // No Way! 

  {
    const Iterator = techInterview();
    const q = Iterator.next() .value; // Iterator yields another question 
    console.log(q);
    const a = Iterator.next('Jimmy Kimmel') .value; 
    // Pass wrong answer back into generator
    console.log(a);
  } 
    // Who is the CEO of Tesla?
    // Jimmy Kimmel
    // No Way!

  { 
    const Iterator = techInterview();
    const q = Iterator.next() .value;  // Iterator yields another question
    console.log(q); 
    const a = Iterator.next('Elon Musk') .value;
    // Pass correct answer back into generator
    console.log(a);
  }
    // Who is the CEO of Tesla? 
    // Elon Musk
    // Okay on to the next question

Conclusion

There are lots of ways asynchronous iterators and generators can improve your work flow. I’ve tried them and it sure worked for me. We don’t have to wait for TC39 to give them the general thumbs up before we begin implementing them (or do we? forgive me please :) ). In the end, we are all learning and what is important is to know what works best for you. I’m just going to leave a couple of resources here for further reading:

Async Iterators and Generators - Jake Archibald Asynchronous Iteration - Dr Axel Rauschmayer Going Async with ES6 Generators - Kyle Simpson

Chris Nwamba

49 posts

JavaScript Preacher. Building the web with the JS community.