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:
- 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. - Calls
.next(): It calls the iterator’s.next()method, which yields a Promise representing the upcoming step. - Awaits Resolution: The execution inside the loop’s scope pauses until the returned Promise settles (fulfills or rejects).
- Evaluates Completion: Once resolved, it checks the
doneproperty:- If
doneisfalse, the value ofvalueis assigned to the loop variable, the loop body executes, and the next cycle begins. - If
doneistrue, the loop terminates.
- If
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
- Sequential Execution: The loop waits for each
Promise to resolve before requesting the next item. If you need
concurrent processing where sequence does not matter, combining
map()withPromise.all()is generally more appropriate. - Scope Requirements: Because it pauses execution
using implicit
awaitoperations,for-await-ofmust be placed inside anasyncfunction or within a top-levelawaitsupported environment.