Volume 2

Q130: Async Iterators, Streams and Backpressure

Difficulty: SeniorFrequency: MediumAnswer time: 10-14 minutes

What Interviewers Want To Evaluate

This question checks whether you understand async data sequences. Interviewers want async iterables, Symbol.asyncIterator, for await...of, readable streams, chunked processing, cancellation, backpressure, and why streaming is different from waiting for a full response.

Senior answers should connect the concept to file uploads, logs, server-sent data, AI streaming, large exports, and responsive UI.

Short Interview Answer

An async iterable produces values over time and exposes Symbol.asyncIterator. It is consumed with for await...of. Streams are a common source of async chunks, such as network response bodies, files, or generated data. Backpressure means the consumer can slow the producer so data does not accumulate faster than it can be processed. Streaming improves responsiveness and memory usage, but it requires careful cancellation, error handling, and partial-state design.

Detailed Interview Answer

Normal iterables produce values synchronously.

Async iterables produce values asynchronously.

for await (const chunk of source) {
  process(chunk);
}

Each iteration can wait for the next value.

Async Iterable Protocol

An async iterable has a method at Symbol.asyncIterator.

const source = {
  async *[Symbol.asyncIterator]() {
    yield "first";
    yield "second";
  },
};

The iterator returns promises for iteration results.

Async Generators

Async generators make async iterables ergonomic.

async function* fetchPages(pageIds) {
  for (const id of pageIds) {
    const page = await fetchPage(id);
    yield page;
  }
}

This lets callers process each page as it arrives.

for await...of

for await (const page of fetchPages(ids)) {
  renderPage(page);
}

The loop waits for each yielded value.

It also handles async iterator cleanup when the loop exits early.

Streams

Streams represent data arriving in chunks.

Examples:

HTTP response body
file read
upload progress
log tailing
AI token streaming
large CSV export

Streaming avoids waiting for the whole payload before work begins.

Web Streams Example

async function readTextStream(response) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let text = "";

  while (true) {
    const { value, done } = await reader.read();

    if (done) {
      break;
    }

    text += decoder.decode(value, { stream: true });
  }

  text += decoder.decode();
  return text;
}

This processes network chunks as they arrive.

Backpressure

Backpressure means the producer respects the consumer's capacity.

Without backpressure:

producer creates chunks quickly
consumer processes slowly
memory buffers grow
app becomes slow or crashes

Stream APIs are designed to help coordinate demand.

Chunked UI Updates

Streaming can make UI feel faster.

show shell
receive first chunk
render partial result
continue appending chunks
finish with final state

This is useful for generated text, logs, search exports, and long-running jobs.

Cancellation

Streaming work must support cancellation.

const controller = new AbortController();

const response = await fetch("/api/stream", {
  signal: controller.signal,
});

controller.abort();

If the user navigates away, closes the modal, or changes the query, the stream should stop or be ignored.

Error Handling

Streams can fail after partial data has already arrived.

The UI needs a partial-state strategy:

discard partial data
show partial data with warning
retry from checkpoint
resume from cursor
mark export as failed

The right behavior depends on the product.

Common Mistakes

A common mistake is buffering the whole stream before rendering, losing the benefit of streaming.

Another mistake is ignoring cancellation and continuing to process chunks after the user left.

Another mistake is assuming every stream can be retried from the beginning safely.

Senior Trade-Offs

Streaming improves perceived speed and memory usage, but it adds complexity.

Use streaming when:

payload is large
first result can be useful early
producer is naturally incremental
memory matters
user benefits from progress

Avoid it when the product needs all-or-nothing validation before showing anything.

Code or Design Example

async function consumeTokens(stream, onToken, signal) {
  for await (const token of stream) {
    if (signal.aborted) {
      break;
    }

    onToken(token);
  }
}

This makes cancellation part of the consumption loop.

Debugging Workflow

When streaming behavior is wrong:

check whether data is buffered accidentally
check chunk decoding
check cancellation on navigation
check partial error behavior
check consumer processing speed
check memory growth from buffers
check retry and resume semantics
verify backpressure support

Interview Framing

Define async iterable and for await...of, then connect it to streams, chunks, cancellation, and backpressure.

Revision Notes

Async iterables produce values over time. Streams are chunked data sources. Backpressure prevents producers from overwhelming consumers.

Key Takeaways

Streaming is a product and architecture decision. It can make interfaces feel alive, but only if partial state, cancellation, and errors are designed clearly.

Follow-Up Questions

  1. What is an async iterable?
  2. What is Symbol.asyncIterator?
  3. What does for await...of do?
  4. What is a stream?
  5. Why stream instead of buffering?
  6. What is backpressure?
  7. How do streams affect memory usage?
  8. How should cancellation work?
  9. What happens if a stream fails after partial data?
  10. When should streaming be avoided?

How I Would Answer This In A Real Interview

I would say async iterables produce values over time and are consumed with for await...of. Then I would explain streams as chunked async data, discuss backpressure, and cover cancellation plus partial error handling.