Ch. 1 · JavaScript

JavaScript Async Iteration and Backpressure

Consume async generators with for await, propagate errors and pace a producer to a slow consumer without buffering the whole stream.

~3 min readadvancedupdated Oct 4, 2026

Synchronous iteration reads values that already exist. Async iteration reads values that arrive over time, such as pages from an API or lines from a socket. for await...of awaits each value before the loop body runs, which is what lets a slow consumer pace a fast producer instead of buffering the whole stream.

Before you start

You should be comfortable with promises, for...of, and generator functions. This example uses an async generator and does not cover Node.js stream objects, cancellation tokens, or the exact scheduling of the event loop. Focus on who decides when the next value is requested.

Step-by-step walkthrough

Step 1: Make the source async iterable

An async generator function (async function*) returns an async iterable. Its Symbol.asyncIterator produces an object whose next() returns a promise of { value, done }. Any source that models “values over time” fits this shape: paginated HTTP results, database cursors, or a queue.

Step 2: Await each value in the loop

for await...of waits for each next() promise before running the body. If the promise rejects, the rejection is thrown into the loop and the loop stops unless you catch it. break, return or a thrown error calls the iterator’s return(), so finally blocks in the generator run and cleanup is not skipped.

Step 3: Let the consumer control the pace

Because the generator only advances when the loop asks for the next value, a consumer that awaits slow work applies backpressure: the producer is not asked for another item until the current one is handled. That guarantee disappears the moment you collect values with Promise.all or push them into an unbounded array.

Worked scenario

Run this with Node.js. Each value is delivered only when the consumer requests it.

async function* fromArray(items) {
  for (const item of items) {
    await Promise.resolve();
    yield item;
  }
}
const seen = [];
for await (const value of fromArray([1, 2, 3])) {
  seen.push(value);
}
console.log(seen); // [1, 2, 3]
JavaScript

Walk through the example

The loop requests one value, fromArray resumes up to the yield, and control returns to the body. The body finishes before the loop asks for the next item, so the generator stays suspended in between. Order is preserved even though the source is async, and the internal awaits do not change that: each next() resolves one value at a time.

Common mistake

Writing array.forEach(async (x) => { await work(x); }) does not wait: forEach ignores the returned promises and the surrounding function continues. For sequencing, use for await...of (or for...of with await). The mirror-image mistake is Promise.all(array.map(async ...)) over an unbounded source, which starts every task at once and removes backpressure entirely.

Verify the behavior

Assert that order is preserved and that the loop stops on break. Add a finally block inside the generator and confirm it runs after an early break. Make the generator throw and confirm the for await loop rejects so a caller can catch it. Time a deliberately slow body and check that the source is not drained ahead of the consumer.

Interview exercise

A teammate says the async loop “runs in parallel because each step is awaited.” Is that correct?

Answer and reasoning

No. for await...of processes values sequentially: the next next() is not requested until the current iteration’s body settles. Parallelism requires explicitly starting several operations and awaiting them together, for example with Promise.all, which is the opposite of backpressure and should be used only when the source is bounded.

Follow-up discussion

Does an async generator need return? Not for normal completion, but implementing finally (or return) makes cleanup deterministic when a consumer stops early. Can you mix sync and async iteration? No: for...of throws on an async iterable, and for await over a plain iterable resolves each value to itself, so be explicit about which protocol a source implements.

Continue learning

Compare this with JavaScript iterators and iterable objects and generator cleanup. For the full protocol, read for await…of and try the JavaScript interview questions.

More in JavaScript

read ✓JavaScript · mid

JavaScript Async Loops and Concurrency

JavaScript Async Loops and Concurrency. Learn the reasoning, a practical example, common mistakes and an interview exercise.

~2 min readread →
read ✓JavaScript · mid

JavaScript Date and Timezone Pitfalls

Parse and format dates without timezone surprises: zero-based months, date-only versus date-time parsing, and why UTC storage avoids drift.

~3 min readread →
esc