Free lesson · JavaScript Basic to Advance
Async Iteration and Generators
Looping Over Async Things
JavaScript provides two features for working with sequences of values over time: async iteration (for await...of) and async generators (functions that produce sequences of async values). Both build on the iteration model from regular for...of loops and the function-with-state model from generators.
async function* numberStream() {
for (let i = 1; i <= 3; i++) {
// Simulate async work between yields
await new Promise(resolve => setTimeout(resolve, 50));
yield i;
}
}
async function consume() {
for await (const num of numberStream()) {
console.log("Got:", num);
}
console.log("Stream done");
}
consume();
The output: Got: 1, then 50ms later Got: 2, then 50ms later Got: 3, then Stream done. The async generator produces values over time, and for await...of waits for each one.
This Topic gives you the essentials. Async iteration is a smaller part of the language than the Promise material, but it shows up in specific scenarios — streaming data, reading file chunks, paginated API results, real-time event processing.
Interactive animation: A generator runs nothing until next() is called, then pauses at each yield and remembers its place.
Generators Without async
Before async generators, let me cover regular generators briefly, because async generators build on them.
A generator is a function that can be paused and resumed. You declare one with function* (the asterisk). Inside, you use yield to produce a value and pause. The caller resumes the generator to get the next value.
function* counter() {
yield 1;
yield 2;
yield 3;
}
const it = counter();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }
// Or use for...of
for (const value of counter()) {
console.log("Value:", value);
}
Each yield produces a value and pauses execution. The next call to next() resumes execution from where the last yield left off. When the function ends, the iterator reports done: true.
Generators are useful for producing sequences lazily — you don't compute all values upfront, just one at a time. The classic example is an infinite sequence:
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
console.log(fib.next().value); // 5
The generator produces Fibonacci numbers indefinitely. You take as many as you need; the rest are never computed.
Interactive animation: An infinite generator of even numbers, pulled five at a time - the rest are never computed.
Async Generators
Async generators combine generators with async. They produce values asynchronously — each yield can wait for a Promise before producing.
async function* fetchPages(maxPage) {
for (let page = 1; page <= maxPage; page++) {
// Simulate fetching a page
const data = await new Promise(resolve => {
setTimeout(() => resolve({ page, items: [`item ${page}.1`, `item ${page}.2`] }), 50);
});
yield data;
}
}
async function main() {
for await (const page of fetchPages(3)) {
console.log("Page:", page);
}
}
main();
Three pages, each fetched asynchronously. The for await...of loop waits for each page before the next iteration begins. This is the canonical pattern for streaming data — you don't load everything upfront, you process it as it comes.
When Async Iteration Helps
Async iteration is useful when:
Data arrives over time, not all at once. Streaming HTTP responses, server-sent events, WebSocket messages, paginated APIs. You can't get all the data in one Promise — you need to consume it as it comes.
Memory matters. Loading a huge dataset all at once might use too much memory. Async iteration lets you process one chunk at a time, with backpressure naturally controlled by how fast you consume.
The source has natural sequencing. Reading a file line by line, processing a queue, walking a directory tree. The source produces values in order, and you want to handle each in turn.
Real-world examples I've worked with at Netflix include:
- Stream processing. Reading from a Kafka topic into JavaScript code. Each message is yielded as it arrives. Backpressure controls how fast we pull.
- Large file processing. Reading multi-gigabyte log files line by line. Loading the whole file would OOM the process; async iteration handles each line.
- Paginated APIs. Fetching pages of search results, with each page yielded as it loads, instead of waiting for the whole result set.
A Practical Pattern
A common pattern: async generator that paginates through an API:
async function* paginate(fetchPage, totalPages) {
for (let p = 1; p <= totalPages; p++) {
const items = await fetchPage(p);
for (const item of items) {
yield item;
}
}
}
async function fakeFetchPage(p) {
await new Promise(r => setTimeout(r, 30));
return [`item ${p}.1`, `item ${p}.2`, `item ${p}.3`];
}
async function example() {
let count = 0;
for await (const item of paginate(fakeFetchPage, 3)) {
console.log(item);
count++;
if (count >= 5) break; // stop early
}
console.log("Done after", count, "items");
}
example();
The generator paginates through 3 pages of 3 items each. The consumer takes the first 5 items and stops. The generator never fetches the third page because the consumer stopped — this is the memory benefit of generators. Work doesn't happen if you don't ask for it.
Interactive animation: An async generator pages through orders behind a simple for await loop, and break stops further pages being fetched.
Common Pitfalls
Warning: The async iteration traps.
Confusing
for await...ofwithfor...ofover Promises.for...ofon an array of Promises doesn't await them — it iterates the Promise objects themselves. To await each, you needfor await...oforPromise.all.Forgetting that async iteration is sequential. Each iteration of
for await...ofwaits for the previous one. If you have independent work,Promise.allparallelizes better.Breaking out without cleanup. When you
breakout of an async iteration, the generator'sreturnmechanism should clean up resources. If your generator opens connections or files, make sure it has atry/finallyto handle early termination.
Why This Matters in Interviews
Interview: Async iteration is a senior-level topic and less commonly asked than core async material. When it comes up, the question is usually "when would you use async generators?" The expected answer involves streaming data, pagination, memory-efficient processing of large datasets.
Output-prediction puzzles involving
for await...ofare rare but do appear. The mechanism is the same asfor...ofbut with anawaiton each iteration.Topic 9.12 — Interview Lens: Promise and async/await Patterns — covers the broader async interview treatment.
Try It
Exercise: Build an async generator
delayedSequencethat:
- Takes a
startvalue, anendvalue, and adelayMsbetween values- Yields each integer from
starttoend(inclusive), withdelayMsbetween eachThen consume it with a
for await...ofloop that prints each value with a timestamp.async function* delayedSequence(start, end, delayMs) { // your code } async function consume() { for await (const value of delayedSequence(1, 5, 100)) { console.log(Date.now(), value); } } consume();The timestamps between values should differ by roughly
delayMs. By the time this works, you've built a real async sequence and consumed it with backpressure.