hardJS ES6+#66

Async generator with for-await-of

Prompt

Create an async generator that yields numbers 1 to 5 with a short delay (50 ms) between each. Write consume() that collects them with for-await-of and returns the array.

Solution

async function* numberGenerator() {
  for (let i = 1; i <= 5; i++) {
    await new Promise(r => setTimeout(r, 50))
    yield i
  }
}

async function consume() {
  const results = []
  for await (const num of numberGenerator()) {
    results.push(num)
  }
  return results
}
Mentor's take

Async generators are pull-based streams: the consumer decides when to ask for the next value, so backpressure is built in — the producer does no work until next() is called. That's the natural shape for paginated APIs: yield each page as it arrives, and the consumer stops pulling when it has enough, so page 7 is never fetched if the caller breaks after page 2. Compare that to eagerly fetching everything and filtering afterwards.

Mechanics: async function* combines both protocols — each next() returns a promise of {value, done}, and for await...of is the loop that awaits each one in sequence. The generator body reads synchronously top-to-bottom while actually suspending twice per iteration: once at await (the delay), once at yield (waiting for the consumer to pull again). The protocol also includes return()for await...of calls it when you break, so a finally block in the generator runs for cleanup (close the connection, abort the request). That cleanup guarantee is what makes generators safer than hand-rolled iterator objects.

Red flag: fetching all pages with Promise.all and then yielding them. It "works" but destroys the two things the pattern exists for — laziness and bounded memory — and signals you reached for a familiar tool instead of understanding the streaming contract.

Say it: "An async generator is a pull-based stream — next() returns a promise of {value, done}, the consumer controls pacing, and for-await-of calls return() on break so my finally cleanup always runs."