Skip to main content

Async generator

An async generator (async function*) is a function that hands out asynchronous values step by step. It creates an async iterator whose .next() method returns a Promise rather than a plain { value, done } object.

Theory

TL;DR

  • Declaration: async function* name() { ... }, that is async plus an asterisk.
  • It returns an async iterator; .next() gives you a Promise<{ value, done }>.
  • Both await and yield are allowed inside, so you can run requests, pauses or database reads between steps.
  • It is iterated only with for await...of; a plain for...of does not work, because it implements [Symbol.asyncIterator].
  • It supports delegation with yield*, plus the control methods .return() and .throw().
  • Typical uses: data streaming, paginated APIs, lazy loading, asynchronous pipelines.

Quick example

javascript
async function* fetchNumbers() { yield 1; yield 2; yield 3; } (async () => { for await (const n of fetchNumbers()) { console.log(n); } })();

Output:

javascript
1 2 3

The difference from a regular function*: every value is returned asynchronously, through a Promise, so the only way to consume it is for await...of.

Async yield: waiting between steps

Async generators earn their keep exactly when there are asynchronous actions between iterations: fetch, await, setTimeout, database queries and so on.

javascript
async function* delayedNumbers() { for (let i = 1; i <= 3; i++) { await new Promise(resolve => setTimeout(resolve, 1000)); // wait 1 second yield i; } } (async () => { for await (const n of delayedNumbers()) { console.log(n); } })();

Output (with a one second pause between the numbers):

javascript
1 2 3

Here the generator waits asynchronously inside await and then hands out the next value with yield. On every iteration for await...of implicitly awaits the result of .next().

What an async generator returns

If you crank the iterator by hand, every .next() has to be awaited:

javascript
const gen = delayedNumbers(); console.log(await gen.next()); // { value: 1, done: false } console.log(await gen.next()); // { value: 2, done: false } console.log(await gen.next()); // { value: 3, done: false } console.log(await gen.next()); // { value: undefined, done: true }

gen.next() returns a Promise that resolves to { value, done }.

An async generator returns an object with the method:

javascript
[Symbol.asyncIterator]()

which returns itself. Such an object can be iterated only with for await...of, never with for...of.

Just like a regular generator, an async one supports delegation with yield*, that is nesting another async generator:

javascript
async function* sub() { yield 1; yield 2; } async function* main() { yield* sub(); // delegates the iteration yield 3; } for await (const v of main()) { console.log(v); }

Output:

javascript
1 2 3

The control methods .return() and .throw() work as well:

javascript
async function* gen() { try { yield 1; yield 2; } catch (e) { console.log('Error inside:', e.message); } finally { console.log('Generator finished'); } } const iterator = gen(); console.log(await iterator.next()); // { value: 1, done: false } console.log(await iterator.throw(new Error("fail"))); // Error inside: fail // Generator finished

Data streams and async pipelines

Async generators are a perfect fit for streaming data processing:

javascript
async function* streamData(urls) { for (const url of urls) { const res = await fetch(url); const data = await res.json(); yield data; } } (async () => { const urls = [ '/api/user', '/api/posts', '/api/comments' ]; for await (const chunk of streamData(urls)) { console.log('Data:', chunk); } })();

Here each fetch() runs in turn, and after every yield the data is handed out into the for await...of loop.

Several generators can be nested into one another like links of a conveyor:

javascript
async function* generateNumbers() { for (let i = 1; i <= 10; i++) { await new Promise(r => setTimeout(r, 200)); yield i; } } async function* filterEven(source) { for await (const n of source) { if (n % 2 === 0) yield n; } } async function* double(source) { for await (const n of source) { yield n * 2; } } // Combining several async generators: (async () => { const pipeline = double(filterEven(generateNumbers())); for await (const value of pipeline) { console.log(value); // 4, 8, 12, 16, 20 } })();

This is lazy asynchronous data processing, essentially a stream pipeline: nothing is computed up front, and every link pulls the next value only when it is asked for.

Where it is used, and a summary

ScenarioHow it is used
Data streamingGradually loading parts of a file or of a server response
Incremental requestsWorking with an API that returns data in "pages"
Lazy computationProducing values as they are needed
Async pipelinesCombining several data sources
Node.js StreamsCompatible with the ReadableStream and AsyncIterator interfaces

Summary:

FeatureDescription
Declarationasync function* name() { ... }
ReturnsAn async iterator
The .next() methodReturns Promise<{ value, done }>
Used withfor await...of
yieldHands out intermediate values asynchronously
ApplicationsData streams, paginated APIs, lazy loading, event generation

The main idea in one line: function* gives synchronous sequences, while async function* gives asynchronous ones, for example "one request at a time".

Common mistakes

  • Iterating an async generator with for...of. That throws, because the object implements [Symbol.asyncIterator], not [Symbol.iterator]; you need for await...of.
  • Forgetting await before .next(). Without it you get a Promise, not { value, done }, and the done check is always undefined.
  • Using for await...of outside an async context. It only works inside an async function or in a module that supports top-level await.
  • Expecting parallelism. An async generator is sequential: the next fetch starts only after the previous value has been taken. For parallel requests you need Promise.all, not a generator.
  • Putting yield inside forEach or another callback. yield only works in the body of the generator itself, so use plain for or for await...of loops.
  • Not closing the generator. If you leave the loop with break or return, the engine calls .return(), so resources such as connections and streams should be released in a finally block.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.