Skip to content

Async Iteration

This lesson explains Async Iteration with clear examples, use cases, and best practices so you can use modern JavaScript confidently in real projects.

ES6 Async Iteration Overview

Async iteration allows JavaScript to iterate over asynchronous data sources using for await...of and asynchronous iterators. Unlike normal iteration, async iteration waits for Promises to resolve before processing the next value.

Async iteration is commonly used when reading streams, processing files, paginated APIs, database cursors, Web Streams, Node.js streams, and other asynchronous data sources.

Feature Description
for await...of Iterates over asynchronous iterables.
Async Iterator Returns Promises from the next() method.
Async Generator Uses async function* and yield.
Works With Streams, APIs, database cursors, files, generators.
Best Use Sequential processing of asynchronous values.

Why Async Iteration?

Normally, JavaScript loops process values that already exist. However, sometimes values arrive over time from APIs, streams, or files.

const numbers =
  [1, 2, 3];

for (const number of numbers) {
  console.log(number);
}

The example above works because all values already exist. Async iteration is required when values become available asynchronously.

Using for await...of

async function processPromises() {
  const promises =
    [
      Promise.resolve(10),
      Promise.resolve(20),
      Promise.resolve(30)
    ];

  for await (const value of promises) {
    console.log(value);
  }
}

processPromises();

Each Promise is awaited before the next iteration begins.

Async Generator Functions

Async generators produce asynchronous values using async function*.

async function* generateNumbers() {
  yield 1;
  yield 2;
  yield 3;
}

async function run() {
  for await (const value of generateNumbers()) {
    console.log(value);
  }
}

run();

Async Generator with Delay

function wait(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function* countdown() {
  for (let i = 3; i >= 1; i--) {
    await wait(1000);

    yield i;
  }
}

async function start() {
  for await (const value of countdown()) {
    console.log(value);
  }
}

start();

Each number is produced after a one-second delay.

Creating an Async Iterator

const asyncCollection =
  {
    values:
      [100, 200, 300],

    async *[Symbol.asyncIterator]() {
      for (const value of this.values) {
        yield value;
      }
    }
  };

async function display() {
  for await (const value of asyncCollection) {
    console.log(value);
  }
}

display();

Objects become asynchronously iterable by implementing Symbol.asyncIterator.

Processing Paginated API Results

async function* getPages() {
  let page = 1;

  while (page <= 3) {
    const response =
      await fetch("/api/users?page=" + page);

    const data =
      await response.json();

    yield data;

    page++;
  }
}

async function loadUsers() {
  for await (const users of getPages()) {
    console.log(users);
  }
}

Async generators are useful for loading paginated API responses one page at a time.

Reading Large Files

async function processChunks(stream) {
  for await (const chunk of stream) {
    console.log(chunk);
  }
}

Browser streams and Node.js streams can be processed chunk by chunk using async iteration.

Node.js Stream Example

import fs from "node:fs";

async function readFile() {
  const stream =
    fs.createReadStream("data.txt");

  for await (const chunk of stream) {
    console.log(chunk.toString());
  }
}

Async iteration makes reading streams much simpler than using event listeners.

Web Streams Example

async function downloadData() {
  const response =
    await fetch("/large-file");

  const stream =
    response.body;

  for await (const chunk of stream) {
    console.log(chunk);
  }
}

This processes downloaded data as it arrives instead of waiting for the complete response.

Returning Values from Async Generators

async function* numbers() {
  yield 10;
  yield 20;

  return 30;
}

async function run() {
  for await (const number of numbers()) {
    console.log(number);
  }
}

The returned value is not included in the for await...of loop.

Error Handling

async function loadProducts() {
  try {
    for await (const product of getProducts()) {
      console.log(product);
    }
  } catch (error) {
    console.error(error.message);
  }
}

Wrap asynchronous iteration in a try...catch block to handle rejected Promises.

Sequential vs Parallel Processing

for await (const item of items) {
  await process(item);
}

The code above processes one item at a time.

await Promise.all(
  items.map((item) =>
    process(item)
  )
);

Promise.all() processes independent tasks in parallel and is usually faster.

Real-World Examples

Upload Multiple Files

async function upload(files) {
  for await (const file of files) {
    console.log(
      "Uploading:",
      file.name
    );
  }
}

Database Cursor

async function processRows(cursor) {
  for await (const row of cursor) {
    console.log(row);
  }
}

Reading Notifications

async function displayNotifications(stream) {
  for await (const notification of stream) {
    console.log(notification);
  }
}

Iteration Comparison

Loop Supports Async Values Use Case
for No Simple arrays.
for...of No Iterables.
forEach() No Array callbacks.
for await...of Yes Async iterables.

Common Async Iteration Use Cases

  • Reading paginated REST APIs.
  • Processing file streams.
  • Reading Node.js streams.
  • Working with Web Streams.
  • Database cursor processing.
  • Event streams.
  • Large dataset processing.
  • Incremental downloads.

Common Async Iteration Mistakes

  • Using for...of instead of for await...of for async iterables.
  • Using await inside forEach().
  • Forgetting that async generators return Promises.
  • Ignoring rejected Promises.
  • Processing independent tasks sequentially when parallel execution is faster.
  • Not closing streams after processing.
  • Creating unnecessary async generators for simple arrays.
  • Not handling cancellation or abort signals.

Async Iteration Best Practices

  • Use for await...of only with asynchronous iterables.
  • Use async generators for streaming data.
  • Handle errors using try...catch.
  • Use Promise.all() for independent tasks.
  • Avoid async work inside forEach().
  • Release streams and resources when finished.
  • Keep generators focused on producing data.
  • Use descriptive async function names.

Key Takeaways

  • for await...of iterates over asynchronous values.
  • Async generators are declared using async function*.
  • Async iterators return Promises from next().
  • Streams and paginated APIs work well with async iteration.
  • Use Promise.all() for parallel work.
  • Use try...catch for error handling.

Pro Tip

Remember this interview rule: for...of works with synchronous iterables, for await...of works with asynchronous iterables. If data arrives over time, use async iteration. If all values already exist, use normal iteration.