Advanced JavaScript: What are Generators and Iterators, and where do they live?

December 26, 2024 (1mo ago)

Iteration allows us to navigate data efficiently, especially when dealing with asynchronous data that arrives on demand, such as in the case of partial downloads. Generators make this task even more convenient by providing an elegant way to pause and resume the execution of functions.

Today, we will learn a little more about an extremely interesting feature that can take you to another level in JavaScript!

What are Iterators and Iterable objects?

Iterable objects are a generalization of arrays, and this generalization allows us, for example, to use objects in a for .. of loop.

Arrays are iterable, and this makes our lives much easier, but, only arrays? No! In fact, any object that represents a collection (lists, sets, vectors) can be a great candidate for iteration, such as strings.

for (const a of 'any'){
    console.log(a)
}
// console log: a
// console log: n
// console log: y

How to use and use case: Pagination

Knowing this, how could we implement pagination using iterators?

class Paginator {
  constructor(items, itemsPerPage) {
    this.items = items;
    this.itemsPerPage = itemsPerPage;
    this.currentPage = 0;
  }

  [Symbol.iterator]() {
    return {
      items: this.items,
      itemsPerPage: this.itemsPerPage,
      currentPage: this.currentPage,
      next() {
        if (this.currentPage * this.itemsPerPage >= this.items.length) {
          return { done: true };
        }
        const start = this.currentPage * this.itemsPerPage;
        const end = start + this.itemsPerPage;
        this.currentPage++;
        return { value: this.items.slice(start, end), done: false };
      }
    };
  }
}

To create an object that takes advantage of iterable implementation, this object must implement the [Symbol.iterator] method. This method will allow our class to be iterated using an iteration structure like for .. of. To implement this method, we must return the next() method, which is necessary because it defines how the iteration should proceed in a sequence of elements. Each call to next must return an object with two properties:

  • done: A boolean that indicates whether the iteration is complete.
  • value: The current value of the iteration (if done is false).

And Async Iterators?

Async iterators are an extension of the iterator concept, allowing iteration over asynchronous data. They are particularly useful when you need to handle I/O operations or network requests that return data asynchronously.

To implement an Async Iterator:

  1. Use Symbol.asyncIterator instead of Symbol.iterator.
  2. The next() method must return a Promise.
    • Just implement the method as async next().
  3. To iterate over such an object, we must use a for await (let item of iterable) loop.
    • Here, we only change the word await.

When implementing an async iterator, the spread operator ... does not work. It only uses iterators.

What are Generators?

Generators are simply a way to implement iterators outside of a class, like a function.

Generators are created using an asterisk when defining a function function* and use yield to generate values, and thus we can use them in a for .. of loop just like iterators.

function* fibonacciGenerator() {
    let a = 0, b = 1;
    while (true) {
        yield a; // Returns the current Fibonacci number
        [a, b] = [b, a + b]; // Calculates the next number in the Fibonacci sequence
    }
}

And here, the use can be the same as iterators! Both using the next method and in a for .. of loop (not exactly this Fibonacci generator because it never ends, huh!)

const fibo = fibonacciGenerator()
console.log(fibo.next()) // {value: 0, done: false}
console.log(fibo.next()) // {value: 1, done: false}
console.log(fibo.next()) // {value: 2, done: false}
console.log(fibo.next()) // {value: 3, done: false}
console.log(fibo.next()) // {value: 5, done: false}

And Async Generators?

Async Generators are a powerful combination of generators and async iterators. They allow you to define functions that can pause execution (yield) and, at the same time, perform asynchronous operations. This is extremely useful for scenarios where you need to process asynchronous data in parts, such as consuming paginated APIs or continuous data streams.

To implement an async generator:

  1. Use async function* to define an async generator.
  2. Use await within the function body and yield to pause execution.
  3. Use for await...of to iterate over the generated values.

Real-life example: All records from a paginated API

Imagine you are consuming a paginated API that returns a set of results on each page, but for some reason, in your use case, you need to return all possible values.

async function* fetchPagedData() {
  let currentPage = 1;
  let hasMoreData = true;

  while (hasMoreData) {
    const response = await fetch(`https://example.com/?page=${currentPage}`);
    const data = await response.json();

    if (data.items.length === 0) {
      hasMoreData = false;
    } else {
      yield data.items;
      currentPage++;
    }
  }
}

To use your async generator:

const pageContents = [];

for await (const items of fetchPagedData(apiEndpoint, pageSize)) {
  pageContents.push(...items);
}

This way, you would have all the items in an extremely elegant manner!


Although the applications of async generators are rare, they can be used in moments such as data streaming, real-time feeds, and large data processing.

So, even if you don't need to use them today, being aware of these techniques is essential so you know what to do when you need to, thus elevating your JavaScript skills to a new level!