Async Iterators
Async iterators are like regular iterators, but each next value may arrive later.
They are useful when data is not ready all at once.
Examples include:
- pages from an API
- chunks from a stream
- messages from a websocket
- log lines from a server
- events from a queue
From Iterator to Async Iterator
A regular iterator has a .next() method that returns a result immediately.
const iterator = [10, 20][Symbol.iterator]();
console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: undefined, done: true }An async iterator also has .next(), but .next() returns a Promise.
const result = await asyncIterator.next();That Promise resolves to the familiar iterator result object:
{
value: "some value",
done: false
}Async Generator Functions
The easiest way to create an async iterator is with an async generator function.
Use async function*.
async function* delayedNumbers() {
yield 1;
await new Promise((resolve) => setTimeout(resolve, 500));
yield 2;
await new Promise((resolve) => setTimeout(resolve, 500));
yield 3;
}This function can await inside its body and yield values over time.
Consuming Async Iterators Manually
You can call .next() yourself.
const numbers = delayedNumbers();
console.log(await numbers.next()); // { value: 1, done: false }
console.log(await numbers.next()); // { value: 2, done: false }
console.log(await numbers.next()); // { value: 3, done: false }
console.log(await numbers.next()); // { value: undefined, done: true }This is useful for understanding the protocol, but most code uses for await...of.
The Async Iterable Protocol
An object is async iterable if it has a Symbol.asyncIterator method.
const asyncRange = {
start: 1,
end: 3,
async *[Symbol.asyncIterator]() {
for (let value = this.start; value <= this.end; value += 1) {
await new Promise((resolve) => setTimeout(resolve, 200));
yield value;
}
},
};
for await (const value of asyncRange) {
console.log(value);
}This makes your own objects work with async iteration syntax.
Fetching Pages One at a Time
Async iterators are especially useful for paginated APIs.
async function* fetchUsersByPage(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}`);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const data = await response.json();
yield data.users;
hasMore = data.hasMore;
page += 1;
}
}The consumer can process one page at a time:
for await (const users of fetchUsersByPage("/api/users")) {
console.log(`Received ${users.length} users`);
}The caller does not need to manage page numbers directly.
Flattening Async Results
Sometimes each yielded value is a batch.
You can yield individual items instead.
async function* fetchUsers(baseUrl) {
for await (const page of fetchUsersByPage(baseUrl)) {
for (const user of page) {
yield user;
}
}
}
for await (const user of fetchUsers("/api/users")) {
console.log(user.name);
}This hides pagination from the consumer.
Error Handling
Use try...catch around the loop.
try {
for await (const user of fetchUsers("/api/users")) {
console.log(user.name);
}
} catch (error) {
console.error("Could not load users:", error);
}If the async generator throws, the loop stops and control moves to catch.
Cleanup With finally
Async generators can clean up resources.
async function* readMessages(connection) {
try {
while (connection.isOpen) {
yield await connection.nextMessage();
}
} finally {
connection.close();
}
}The finally block can run when:
- the iterator finishes
- the loop breaks early
- an error occurs
Common Mistakes
Do not use regular for...of with an async iterator.
for (const value of delayedNumbers()) {
console.log(value); // TypeError
}Use:
for await (const value of delayedNumbers()) {
console.log(value);
}Do not forget that async iteration is sequential by default.
for await (const user of fetchUsers("/api/users")) {
await saveUser(user);
}This waits for each saveUser() before moving to the next user.
That may be correct, but it is not parallel.
Best Practices
- Use async iterators when values arrive over time.
- Keep each yielded value meaningful, such as one item or one page.
- Handle errors around the consuming loop.
- Use
finallyfor cleanup in generators that hold resources. - Be clear about whether work should be sequential or concurrent.
Summary
Async iterators represent values that arrive asynchronously.
Async generator functions use async function*, can await, and can yield results over time.
They are a clean way to model paginated data, streams, queues, and other async sources.