Course contentsShow
← Node.js
Lesson 214 of 1,9803. Asynchronous Patterns and Flow ControlFree lesson

Building Custom Async Iterables

You will implement the Symbol.asyncIterator interface to make custom objects natively iterable in for-await loops.

In this lesson, you will learn how to create your own objects that can be natively iterated using for await...of loops by implementing the Symbol.asyncIterator method.

The "Contract" of Async Iteration

Up until now, you’ve used async generators to produce data. But sometimes, you want to turn a standard class or object into a "collection" that someone else can loop over. To do this, an object must follow a specific contract: it needs a property named [Symbol.asyncIterator] that returns an object containing a next() method.

This next() method must return a Promise that resolves to an object with a value and a done property. When you follow this structure, Node.js automatically knows how to handle the object in a for await...of loop.

Implementing the Interface

Think of a custom async iterable as a data source that "decides" how it releases its items. Here is how you implement it manually:

const myDataSource = {
  [Symbol.asyncIterator]() {
    let count = 0;
    return {
      async next() {
        if (count < 3) {
          // Simulate an async operation (like a DB fetch)
          await new Promise(resolve => setTimeout(resolve, 500));
          return { value: count++, done: false };
        }
        return { done: true };
      }
    };
  }
};

(async () => {
  for await (const item of myDataSource) {
    console.log(item); // Prints 0, 1, 2 with 500ms delays
  }
})();

Why Do This Manually?

While generators are often easier for writing simple loops, implementing [Symbol.asyncIterator] manually gives you total control over the object's state and behavior. It is the architectural foundation for building custom data structures—like paginated API clients, streaming file readers, or custom event queues—that feel like native arrays to the rest of your application. By implementing this protocol, you hide the complexity of "how" the data is fetched, allowing the consumer to focus purely on "what" to do with the data once it arrives.

Key Takeaway: By defining a [Symbol.asyncIterator] method that returns a next() function, you grant any custom object the ability to be consumed natively via for await...of loops.