Lesson 54 of 80
Regular generators yield values one at a time, synchronously. Async generators yield promises of values — the natural shape for paginated APIs and streamed data.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A regular generator (function*) pauses and resumes synchronously — every yield hands back a value immediately. That's fine when producing the next value is instant, but useless when producing the next value means waiting on something: another page of API results, the next chunk of a file, the next message off a socket.
An async generator (async function*) is the two ideas combined: it's still a generator — you still get pause/resume and a lazy sequence — but each step can await before it yields, and consuming it uses for await...of instead of a plain for...of. The loop automatically awaits each yielded value for you.
A typical paginated API gives you one page plus a cursor for the next one. Without an async generator you'd hand-roll a loop with manual state; with one, the pagination logic lives in one place and callers just iterate:
async function* fetchAllUsers(baseUrl) {
let cursor = null;
while (true) {
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
const res = await fetch(url);
const { users, nextCursor } = await res.json();
yield* users; // yield each user individually, not the whole page
if (!nextCursor) return;
cursor = nextCursor;
}
}
for await (const user of fetchAllUsers("/api/users")) {
console.log(user.name);
if (user.name === "target") break; // stops fetching further pages entirely
}
Two things make this worth the ceremony over just fetching everything up front. First, memory: only one page is ever in memory at a time, not the entire dataset. Second, and more subtly, the break actually stops work — because the generator is lazy, the next page is only fetched when the loop asks for the next value, so bailing out early genuinely avoids the remaining network calls, not just the remaining iterations.
for await...of also works on plain (non-async) iterables of promises. You don't strictly need an async generator to use it — an array of promises works too, awaiting each one in sequence. The async generator is what makes producing those values lazy and on-demand rather than kicking them all off at once.fetch or thrown error inside fetchAllUsers propagates out to whatever try/catch wraps the for await loop — no separate error-handling channel to learn..next() returns a promise of { value, done }, not the plain object a sync generator returns. That's exactly why for await...of exists — a plain for...of would hand you unresolved promises instead of awaited values.This is generators' pause-and-resume model and promises' "wait for this to finish" model, running at the same time instead of one after the other. Nothing new is invented here — it's the iterator protocol from the generators lesson, with every step now allowed to be asynchronous, which is exactly the shape a fetch-based ReadableStream reader also takes when you consume it chunk by chunk.