How Symbol.asyncIterator Works in JavaScript
Symbol.asyncIterator is a built-in JavaScript symbol
that defines the asynchronous iteration protocol for an object. By
implementing a method with this symbol, an object can yield values
asynchronously over time, allowing it to be consumed by the
for await...of loop. This mechanism allows developers to
iterate over data sources that produce results asynchronously—such as
data streams, paginated API responses, or delayed events—using a clean
and declarative syntax instead of relying on manual callback chaining or
complex promise management.
The Asynchronous Iteration Protocol
To understand Symbol.asyncIterator, it helps to compare
it to standard synchronous iteration:
- Synchronous Iteration
(
Symbol.iterator): Thenext()method immediately returns an object in the format{ value: any, done: boolean }. - Asynchronous Iteration
(
Symbol.asyncIterator): Thenext()method returns a Promise that resolves to an object in the format{ value: any, done: boolean }.
When an object has a [Symbol.asyncIterator] method,
JavaScript marks it as an AsyncIterable.
How
for await...of Interacts with the Symbol
The primary consumer of Symbol.asyncIterator is the
for await...of loop. When the loop executes, the runtime
performs the following steps:
- It calls the object’s
[Symbol.asyncIterator]()method to retrieve the async iterator instance. - It invokes
.next()on the iterator, which returns a Promise. - It awaits the Promise resolution.
- If
doneisfalse, it assignsvalueto the loop variable and executes the loop body. - It repeats steps 2–4 until the resolved object has
done: true.
Implementing a Custom Async Iterable
You can define [Symbol.asyncIterator] manually on any
custom object:
const asyncCounter = {
max: 3,
[Symbol.asyncIterator]() {
let current = 1;
const max = this.max;
return {
async next() {
if (current <= max) {
// Simulate an asynchronous operation
await new Promise((resolve) => setTimeout(resolve, 500));
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
(async () => {
for await (const num of asyncCounter) {
console.log(num); // Logs 1, 2, 3 with a 500ms delay between each
}
})();Using Async Generators
Writing manual iterators with boilerplate next() methods
can be tedious. JavaScript provides Async Generator
Functions (async function*), which automatically
implement Symbol.asyncIterator under the hood:
async function* fetchPages(urls) {
for (const url of urls) {
const response = await fetch(url);
const data = await response.json();
yield data;
}
}
// Consuming the generator
(async () => {
const pages = fetchPages(['/api/page/1', '/api/page/2']);
for await (const page of pages) {
console.log(page);
}
})();In this example, calling fetchPages() returns an
AsyncGenerator object that inherently satisfies the async iterable
protocol through Symbol.asyncIterator.
Practical Use Cases
- Node.js Streams: Readable streams in Node.js (and
the browser Streams API) implement
Symbol.asyncIterator, enabling you to read incoming chunks of data directly withfor await...of. - Database Pagination: Fetching large query results in chunks or cursors without loading the entire dataset into memory at once.
- Real-time Feeds: Consuming incoming WebSocket messages or server-sent events sequentially as they arrive.