Async Iterators & Streams

10 questions found

What is an async iterator?

Beginner
An async iterator is an object with a next() method that returns a Promise resolving to {value, done}, letting you iterate over values that arrive asynchronously over time, like chunks from a network stream.
const iterator = someAsyncIterable[Symbol.asyncIterator]();
const { value, done } = await iterator.next();
Real-world example Reading lines from a large file or paginated API results one chunk at a time.

Common follow-ups: How is Symbol.asyncIterator different from Symbol.iterator?

Iterators & Generators

How do you loop over an async iterable?

Beginner
Use a for-await-of loop, which automatically awaits each next() call and unwraps the resolved value, pausing the loop until each item is ready.
async function run() {
  for await (const chunk of asyncIterable) {
    console.log(chunk);
  }
}
Real-world example Streaming and logging each chunk of a fetch() response body as it arrives.

Common follow-ups: Can you use for-await-of on a regular (synchronous) array?

Promises & async/await

How do you write an async generator function?

Intermediate
Combine function* with async: async function*. Each yield can await a value first, and calling the generator returns an async iterable you can consume with for-await-of.
async function* fetchPages(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const data = await res.json();
    yield data.items;
    next = data.nextPage;
  }
}
Real-world example Lazily fetching paginated API results page-by-page, only when the consumer asks for more.

Common follow-ups: How would you stop consuming an async generator early?

Iterators & Generators

What is the ReadableStream API used for?

Intermediate
ReadableStream represents a source of data (like a network response body) that can be read incrementally in chunks, rather than waiting for the entire payload to load into memory at once.
const response = await fetch(url);
const reader = response.body.getReader();
const { value, done } = await reader.read();
Real-world example Displaying a progress bar while downloading a large file, based on bytes received so far.

Common follow-ups: How do ReadableStream and async iterators relate to each other?

Networking: Fetch XHR WebSockets & CORS

What does return() do on an async iterator?

Intermediate
return() is called automatically when a for-await-of loop exits early (via break, return, or an exception), giving the iterator a chance to run cleanup code such as closing a file handle or network connection.
async function* gen() {
  try {
    yield 1; yield 2; yield 3;
  } finally {
    console.log('cleanup ran');
  }
}
for await (const v of gen()) { if (v === 2) break; }
// logs 'cleanup ran'
Real-world example Ensuring a database cursor or file stream is closed even if the consumer stops iterating early.

Common follow-ups: Does the same cleanup behavior apply to synchronous generators?

Error Handling

How do you convert a Node.js Readable stream into an async iterable?

Advanced
Modern Node.js Readable streams already implement Symbol.asyncIterator directly, so you can for-await-of over them without any extra wrapping.
const fs = require('fs');
async function readFile() {
  const stream = fs.createReadStream('data.txt');
  for await (const chunk of stream) {
    console.log(chunk.toString());
  }
}
Real-world example Processing a large log file line-by-line without loading the whole file into memory.

Common follow-ups: How would you handle backpressure when the consumer is slower than the source?

Memory Management & Garbage Collection

How do you run multiple async iterators concurrently and merge their results?

Advanced
There's no built-in merge, so you typically race each iterator's next() Promise against the others, yielding whichever resolves first and re-queuing that iterator's next call — libraries like RxJS provide this out of the box.
async function* merge(...iters) {
  const its = iters.map(i => i[Symbol.asyncIterator]());
  const promises = its.map((it, i) => it.next().then(r => ({ i, r })));
  // race, yield, and refill promises[i] in a loop
}
Real-world example Combining live updates from multiple WebSocket feeds into a single unified event stream.

Common follow-ups: Why is manual merging harder than it looks at first glance?

Event Loop & Concurrency

What's the difference between piping streams with pipeThrough() and pipeTo()?

Advanced
pipeTo() sends a ReadableStream's data into a WritableStream and returns a Promise that resolves when piping finishes. pipeThrough() sends data through a TransformStream (readable + writable in one) and returns a new ReadableStream you can keep chaining or reading further.
const transformed = response.body
  .pipeThrough(new TextDecoderStream())
  .pipeThrough(new SomeTransform());
await transformed.pipeTo(writableStream);
Real-world example Decoding, transforming, and compressing a file stream in one pipeline without buffering it all in memory.

Common follow-ups: How do you handle errors that occur mid-pipeline?

Networking: Fetch XHR WebSockets & CORS

Why might Promise.all() be the wrong tool when consuming many async iterators?

Advanced
Promise.all() waits for ALL promises to settle before giving you anything, so if you're driving async iterators through it you lose the ability to process results as they arrive — defeating the point of streaming. Use for-await-of or manual racing for incremental results.
// Wrong: loses streaming benefit
const all = await Promise.all(iterators.map(it => it.next()));

// Better: process each as it resolves
for await (const v of mergeAsyncIterables(...iterators)) { /* ... */ }
Real-world example Showing search results from multiple providers as each one responds, instead of waiting for the slowest.

Common follow-ups: When IS Promise.all() the right choice for iterator-like data?

Promises & async/await

How do you add a timeout to an async iterator's next() call?

Advanced
Race the iterator's next() Promise against a Promise that rejects after a timeout using Promise.race(), so a slow or stalled source doesn't hang your consumer forever.
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('timeout')), ms));
  return Promise.race([promise, timeout]);
}
const { value } = await withTimeout(iterator.next(), 5000);
Real-world example Cutting off a stalled WebSocket or SSE connection after a set idle period.

Common follow-ups: How would you also clean up the timer if the original promise wins the race?

Error Handling