Mastering Async Generators

Introduction: Combining Generators and Async

In our previous lessons, we explored two powerful but separate worlds. First, we learned about generators, which allow us to produce values lazily, one at a time. Then, we moved into the world of asynchronous programming, using Promises and async/await to handle tasks that take time, like waiting for a timer or fetching data.

Up until now, our generator pipelines have been synchronous. This means that as soon as we asked for a value, the generator calculated it immediately. However, in modern web development, data often arrives in a "stream" over time. Imagine a social media feed where new posts appear every few seconds or a stock ticker where prices update constantly. To handle these situations, we need to combine generators with asynchronous logic. In this lesson, we will learn how to create and consume Async Generators, which allow us to produce and process sequences of data that arrive at their own pace.

Defining Async Generator Functions

To create a generator that can handle asynchronous tasks, we use the async function* syntax. This is a special type of function that combines the behavior of an async function with a generator. While a regular generator function returns an iterator, an async generator returns an Async Iterator.

A key technical detail to remember from our first lesson is that JavaScript uses special symbols to define how objects behave. While regular iterables use Symbol.iterator, these new async generators automatically implement Symbol.asyncIterator. This tells JavaScript that when we request the next value from the generator using the .next() method, it will return a Promise instead of the value object itself.

async function* simple() {
  yield "Async Data";
}

const it = simple();

// Without await: calling .next() returns a Promise
const response = it.next();
console.log(response); // Promise { <pending> }

// With await: we get the iterator result object { value, done }
(async () => {
  const result = await it.next();
  console.log(result); // { value: undefined, done: true }
})();

Yielding Values Over Time

The real power of an async generator comes from using the yield keyword together with await. In a standard generator, yield pauses the function and sends a value back to the caller. In an async generator, we can yield await the result of a Promise. This allows the generator to wait for an asynchronous task to finish before it provides the next value to the consumer.

const delay = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

async function* asyncCounter(limit, gap = 10) {
  for (let i = 1; i <= limit; i++) {
    yield await delay(gap, i);
  }
}

In this code snippet, we define a helper function called delay that returns a Promise resolving after a specific amount of time. Inside the asyncCounter generator, we use a for loop to count up to a limit. On each step, we await the delay function. This causes the generator to pause for the duration of the gap before it yields the current number. Each value yielded by this generator is wrapped in a Promise, meaning the consumer will only receive the number once the timer has finished.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal