How for-await-of Works in JavaScript

JavaScript’s for-await-of loop is a control flow statement introduced in ES2018 that allows developers to iterate sequentially over asynchronous data sources, such as streams, paginated API responses, and async generators. Unlike standard iteration loops, for-await-of automatically pauses loop execution until each retrieved Promise resolves, providing clean, readable syntax for consuming asynchronous sequences without complex chain-based handlers.

The Underlying Mechanism: Async Iterables

To understand for-await-of, you must first understand async iterables. An object is considered an async iterable if it implements the [Symbol.asyncIterator] method.

While a standard synchronous iterator’s next() method returns an object in the format { value, done }, an asynchronous iterator’s next() method returns a Promise that resolves to { value, done }.

const asyncIterable = {
  [Symbol.asyncIterator]() {
    let count = 0;
    return {
      async next() {
        if (count < 3) {
          return { value: count++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

Step-by-Step Execution Flow

When a for-await-of loop executes, JavaScript performs the following steps on each iteration:

  1. Retrieves the Iterator: It invokes the [Symbol.asyncIterator]() method on the target object to obtain the async iterator. If [Symbol.asyncIterator] is absent, it falls back to the standard [Symbol.iterator], wrapping yielded values in resolved Promises.
  2. Calls .next(): It calls the iterator’s .next() method, which yields a Promise representing the upcoming step.
  3. Awaits Resolution: The execution inside the loop’s scope pauses until the returned Promise settles (fulfills or rejects).
  4. Evaluates Completion: Once resolved, it checks the done property:
    • If done is false, the value of value is assigned to the loop variable, the loop body executes, and the next cycle begins.
    • If done is true, the loop terminates.

Practical Usage with Async Generators

The most common way to create async iterables is using asynchronous generator functions (async function*), which combine async/await with generator syntax (yield).

async function* fetchPages(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    const data = await response.json();
    yield data;
  }
}

async function processAll() {
  const pages = fetchPages(['/api/page1', '/api/page2', '/api/page3']);
  
  for await (const page of pages) {
    console.log('Received page data:', page);
  }
}

In this pattern, network requests are not executed all at once. Instead, each request is triggered and waited on demand, reducing memory overhead and managing system backpressure efficiently.

Error Handling

Errors inside a for-await-of loop are caught using standard try...catch blocks. If any Promise returned by .next() rejects, or if an error is thrown inside the iterator logic, execution jumps immediately to the catch block.

try {
  for await (const chunk of readableStream) {
    processChunk(chunk);
  }
} catch (error) {
  console.error('Stream processing failed:', error);
}

If the loop terminates prematurely due to a break, return, or an unhandled exception, JavaScript invokes the iterator’s .return() method (if defined) to allow cleanup operations, such as closing file descriptors or network sockets.

Key Considerations