You call `.next()` on an async generator a second time before the promise from the first `.next()` has settled. What does the async generator do with that second call, and what does that imply about prefetching the next item?
answer
- requests are queued, not concurrent
- body is never resumed reentrantly
- promises settle in call order
- one program counter, one frame
- look-ahead belongs inside the body
basics
~20 sThe second call is queued. An async generator keeps an internal queue of pending requests and serves them strictly in order, never resuming its body reentrantly, so overlapping next() calls buy you no concurrency — the generator is serial by construction.
solid answer
~50 sEach async generator holds an internal queue of pending requests. A `next()` arriving while the body is still suspended on an `await` does not resume it a second time; the request is enqueued and served only after the current one completes, and the promises settle in call order. So firing `next()` three times in a row does not run three fetches concurrently — it just registers three claims on a strictly sequential producer. That is the property that makes async generators safe as a pull-based interface, but it means you cannot parallelise from the *consumer* side. If you want request N+1 in flight while the consumer processes item N, start it *inside* the body: kick off the next fetch, then yield the current value, and await the stored promise on the next iteration. Anything beyond one item of look-ahead belongs in a buffering wrapper around the generator, not in the protocol.
code
javascript · 15 linesconst fetchPage = async (n) => `page${n}`;
async function* prefetched() {
let pending = fetchPage(0);
for (let n = 1; n < 3; n++) {
const current = await pending;
pending = fetchPage(n); // request N+1 starts before the consumer sees N
yield current;
}
yield await pending;
}
(async () => {
for await (const p of prefetched()) console.log(p); // page0, page1, page2
})();go deeper
Know that you should await each next() before calling it again, and that an async generator produces one item at a time rather than several in parallel.
Explain the queue: overlapping requests are enqueued and answered in call order, the body is never resumed reentrantly, and results are therefore strictly sequential.
Show how to get real overlap where it matters — start the next request inside the body before yielding, and handle the in-flight promise on early exit so it cannot become an unhandled rejection.
Judge when the pull protocol is the wrong tool entirely: for independent high-throughput work, own the concurrency limit explicitly rather than bending a sequential producer into a parallel one.
## The request queue An async generator is not just a sync generator with promises bolted on. Each one maintains an internal queue of outstanding requests — one entry per `next()`, `return()`, or `throw()` call that has not yet been answered. When a request arrives: - if the generator is idle, it is resumed with that request immediately; - if the generator is already executing (suspended at an `await` inside its body, mid-resumption), the request is appended to the queue and the caller gets a promise that settles later. When the current resumption produces a result, that result settles the front request's promise, and the generator picks up the next queued request, if any. The consequence is that the body is **never** resumed reentrantly, and results are delivered in the order the calls were made. ```js async function* slow() { yield await new Promise((r) => setTimeout(() => r("a"), 50)); yield await new Promise((r) => setTimeout(() => r("b"), 50)); } const it = slow(); const p1 = it.next(); const p2 = it.next(); // queued, not concurrent Promise.all([p1, p2]).then(([x, y]) => console.log(x.value, y.value)); // a b, ~100ms ``` Two calls issued at the same instant still take the sum of both waits, not the max. There is no error and no warning — just no parallelism. ## Why the protocol is built that way A generator body has one program counter and one set of local variables. Resuming it twice at once would mean two logical executions sharing a single mutable frame — incoherent for any body with state, which is nearly all of them. Serialising requests is what lets you write the body as ordinary sequential code, with loops and locals and `try`/`finally`, and reason about it the same way you reason about an async function. It is also what gives pull-based iteration its natural pacing: the producer advances only when someone asks, so it cannot outrun the consumer or build up an unbounded internal buffer. That is a property, not an accident. ## Getting look-ahead where you actually want it Since the consumer cannot force overlap, the producer must express it. The standard trick is to start the request for the next item *before* handing out the current one: ```js const fetchPage = async (n) => `page${n}`; async function* prefetched() { let pending = fetchPage(0); for (let n = 1; n < 3; n++) { const current = await pending; pending = fetchPage(n); // in flight while the consumer works on `current` yield current; } yield await pending; } ``` Now the network request for page N+1 overlaps the consumer's processing of page N, cutting the wall-clock time of a sequential drain roughly in half when fetch and processing costs are comparable. Note what this costs: the generator now performs one request the consumer may never ask for, so if the consumer breaks early you have paid for an extra fetch — and, more seriously, that in-flight promise must not be left unobserved on an early exit, or a rejection from it becomes an unhandled rejection. A `finally` that awaits and swallows the outstanding promise handles that. For deeper look-ahead — keep k items buffered ahead of the consumer — the honest answer is that this is no longer the generator's job. You wrap it: a separate component drains the source generator into a bounded buffer and exposes its own async iterable, refilling as the consumer drains. That wrapper, not the protocol, is where the buffer bound and the extra-work-on-abandon policy live. ## What this rules out A few patterns people reach for and should not: - Calling `next()` in a loop without awaiting, expecting concurrent production. You get a queue, and if the generator is infinite you get an unbounded queue of pending promises. - Sharing one async generator between two consumers that both pull. They will interleave arbitrarily depending on who calls first; each item goes to exactly one of them. If you need fan-out, tee the sequence explicitly. - Using `Promise.all` over several `next()` calls to "parallelise" a single generator. It parallelises nothing; it only waits for a batch of sequential results. When the work genuinely is parallel — N independent URLs with no ordering dependency — a generator is the wrong producer shape for the fetching itself. Fetch concurrently with an explicit concurrency limit and use the generator, if at all, to deliver results as they land.
- What is the risk of the in-body prefetch pattern when the consumer breaks early?You have one request in flight that nobody will consume. Two problems follow: you paid for work that was not needed, and if that promise rejects with no handler attached it surfaces as an unhandled rejection. Handle both in a `finally` that awaits the outstanding promise and swallows its failure, so the abandoned request settles quietly.
- Can two consumers safely pull from the same async generator?They will not corrupt it — requests are queued and served in order — but each item goes to exactly one consumer, chosen by who called first, so neither sees the whole sequence. That is almost never what people intend. If you need both to see everything, tee the source explicitly into two buffered iterables.
- When is an async generator simply the wrong producer for parallel work?When the items are independent and the goal is throughput — say fetching fifty unrelated URLs. The generator body is sequential by construction, so it caps you at one in-flight operation. Run those with an explicit bounded-concurrency driver instead; a generator can still be the delivery surface for results as they complete, but it should not own the fetching.
saying these in an interview costs you the question
- Thinks overlapping next() calls run the body concurrently
- Uses Promise.all on next() calls to parallelise
- Expects the second next() to throw or be dropped
- Believes the generator buffers ahead automatically
- Assumes two consumers each receive every item