Async iteration & async generators
Loop over values that arrive over time with Symbol.asyncIterator, for await...of, async generators, paginated APIs, streams, and Array.fromAsync.
- 01Read the protocolRecognize an object whose
next()returns a promise of{ value, done }. - 02Consume async dataUse
for await...ofwith async generators, streams, and sync iterables of promises. - 03Hide messy loopsWrap pagination and cleanup in async generators that callers can break from safely.
Values that arrive later
Arrays are easy to loop over because every value is already in memory. Many real sequences are not like that. A server sends page one before page two, a stream produces chunk one before chunk two, and a reader may need to wait before the next value exists.
Async iteration is JavaScript’s protocol for those sequences over time. It says: “ask me for the next value, and I’ll return a promise that settles when the value is ready.” The consumer still sees a simple loop, but each turn can wait.
A normal iterator is like reading messages already on your screen. An async iterator is a chat waiting for the next message. You ask now, read it when it arrives, then wait for the next one.
- In real life: The next message arrives later
- In JavaScript: A value becomes ready later
- In real life: You read one new message
- In JavaScript: The loop calls
next() - In real life: You wait for the next message
- In JavaScript:
next()returns a promise - In real life: The chat has no more messages
- In JavaScript:
{ done: true }
Where the analogy stops: A chat can have messages from many people. One async iterator still produces one ordered sequence; it does not make independent work parallel.
You will build the protocol by hand, step through a for await...of loop, hide paginated requests in async generators, read a real in-page stream, collect values with Array.fromAsync, and choose the right tool for common tasks.
The async iterator protocol & Symbol.asyncIterator
INTERACTIVEA normal iterable has a Symbol.iterator method. An async iterable has a Symbol.asyncIterator method. The iterator it returns has a next() method, and that method returns a promise for the familiar iterator result: { value, done }.
| Feature | Sync iterator | Async iterator |
|---|---|---|
| Method key | Symbol.iterator | Symbol.asyncIterator |
next() returns | { value, done } | A promise for { value, done } |
| Loop | for...of | for await...of |
| Best for | Values already available | Values that may arrive later |
The lab below writes the protocol without generator syntax. It also shows a useful edge case: for await...of can consume a normal sync iterable, such as an array, and await each value that comes out of it.
const mailbox = { letters: ["invoice", "postcard"], index: 0, [Symbol.asyncIterator]() { return this; }, next() { if (this.index === this.letters.length) { return Promise.resolve({ done: true }); } const value = this.letters[this.index++]; return Promise.resolve({ value, done: false }); },}; for await (const letter of mailbox) { console.log(letter);}const tasks = [ Promise.resolve("wake"), Promise.resolve("brew"), "sip",]; for await (const task of tasks) { console.log(task);}The object uses [Symbol.asyncIterator] and next(). The array example proves for await also accepts sync iterables and awaits each value.
for await...of is allowed inside async functions and at module top level. A classic script or ordinary function cannot pause this way.
for await...of: wait, handle, repeat
STEP THROUGHfor await...of is the consumer side. Behind the friendly syntax, the loop asks for the next value, awaits the promise, runs the body, then asks again. That means loop bodies run one at a time, in order.
Imagine waiting by the door and opening each letter as it arrives. You do not read tomorrow’s letter today. If you leave early, you put up a sign so delivery can stop cleanly.
- In real life: Stand by the door
- In JavaScript: Start the
for awaitloop - In real life: Open one letter as it drops
- In JavaScript: Run the loop body for one value
- In real life: Wait for the next letter
- In JavaScript: Await the next
next()promise - In real life: Put up a no-more-mail sign
- In JavaScript:
breakcalls cleanup throughreturn()
Where the analogy stops: A person might open several letters at once. A single for await loop handles one value, then the next. Use Promise.all for independent parallel work.
Step through the recorded run. Switch between consuming all values and breaking early. Notice that line 6 runs either way because the async generator has a finally block.
Step through each await, yield, loop body, and cleanup moment.
script
async function* letters() { try { yield await Promise.resolve("A"); yield await Promise.resolve("B"); yield await Promise.resolve("C"); } finally { console.log("cleanup"); }} console.log(letter); if (false) break;}Async generators: async function*
Writing the protocol by hand is useful for understanding it, but most code uses async function*. Calling an async generator function creates an async iterator. Its body does not run immediately; it starts when the consumer asks for the first value.
Inside the body, await waits for data and yield hands one value to the consumer. The generator pauses at that yield until the consumer asks again. If code calls next() repeatedly before previous calls settle, the async generator queues those requests and still runs its body in order.
If an async generator throws, the pending next() promise rejects. In a for await...of loop, that rejection is thrown at the loop, where you can catch it with try/catch.
Paginated APIs: hide the fetching loop
INTERACTIVEPaginated APIs are a perfect async-generator example. A response gives you some items and a hint about whether another page exists. The next request depends on the previous response, so a sequential loop is the honest shape.
You borrow one book, then ask whether another is ready. An async generator gives callers that simple routine while hiding page counters, response parsing, and cleanup.
- In real life: Borrow the first book
- In JavaScript: Fetch page one
- In real life: The librarian says another book is ready
- In JavaScript:
hasMoreor a next-page token - In real life: Ask for the next book
- In JavaScript: Consumers loop over
items() - In real life: Stop borrowing
- In JavaScript:
breakstops future requests
Where the analogy stops: A library can find many books at once. A cursor-based API usually needs the previous page before it can request the next one.
async function fetchPage(page, pageSize) { const response = await fakeApi(page, pageSize); return response.json();} async function* pages(pageSize) { try { for (let page = 1; ; page += 1) { const data = await fetchPage(page, pageSize); yield data; if (!data.hasMore) return; } } finally { console.log("closed pagination cursor"); }} async function* items(pageSize) { for await (const page of pages(pageSize)) { yield* page.items; }}The generator requests the next page only when the loop needs more items. If the loop breaks, finally runs and later pages are never requested.
Response objects after real promise delays inside this page.Try stopping after fewer items than the total. The request log shows which pages were never fetched, and the cleanup row shows the finally block running.
Streams: chunks over time
INTERACTIVEBrowser streams also produce a sequence over time. Fetch responses, compression tools, and file APIs can expose ReadableStream chunks. Some environments let you consume a stream directly with for await...of. Others require the older reader.read() loop, so production code should feature detect.
const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("alpha")); controller.enqueue(new TextEncoder().encode(" beta")); controller.close(); },}); let text = "";for await (const chunk of stream) { text += new TextDecoder().decode(chunk, { stream: true });}console.log(text);The stream is created in the page. Newer browsers can iterate it directly; older ones can still use a reader loop.
This lab builds a stream inside the page. It checks stream async iteration after mount, then reports whether your browser used direct iteration or the fallback reader loop. For more download-progress patterns, see the Streams & progress lesson.
Array.fromAsync: collect the sequence
INTERACTIVESometimes you do not want to handle each value as it arrives. You want all of them in an array. Array.fromAsync(iterable) returns a promise for an array containing every value from an async iterable, or every awaited value from a sync iterable. It is available in modern browsers and Node 22+, so feature-check if you target older runtimes.
async function* oneByOne() { yield await wait("A", 30); yield await wait("B", 30); yield await wait("C", 30);} const fromGenerator = await Array.fromAsync(oneByOne());const promises = [wait("A", 30), wait("B", 30), wait("C", 30)];const fromPromises = await Array.fromAsync(promises);const allAtOnce = await Promise.all(promises);console.log(fromGenerator.join(""));console.log(fromPromises.join(""));console.log(allAtOnce.join(""));Collecting an async generator pulls one value at a time. An array of already-created promises has already started its work, so collecting it and Promise.all observe the same fulfilled values.
Array.fromAsync is available in modern browsers and Node 22+. Feature-check before using it on older targets.The timing lesson is subtle: collecting an async generator pulls one value at a time. An array of promises may already have started all its work. For truly independent operations, the Promise combinators lesson and Sequential vs parallel lesson explain why Promise.all is often faster.
Where you will use this
Async iteration appears whenever code needs a clean consumer for a producer that waits. Real front-end examples include loading search results page by page, reading a streamed response from fetch, processing uploaded file chunks, or adapting a WebSocket into a sequence of messages.
But it is not the only tool. Sort each task by the simplest honest loop shape.
- Read every page from an API that gives a
nextPagetoken - Decode chunks from a
ReadableStreamas they arrive - Fetch 20 independent profile images
- Load independent config files before rendering
- Add a class to each already-loaded DOM node
- Sum numbers that are already in an array
Put each task under the tool that matches its data shape.
Common misconceptions
“for await runs all promises in parallel.”
No. It awaits one produced value at a time. Use Promise.all when independent work should run together.
“Breaking a loop just stops the body.”
Breaking also calls the iterator’s return() method when one exists. Async generators use that to run finally cleanup.
“Every stream supports direct async iteration.”
Not in every browser version. Feature-detect ReadableStream.prototype[Symbol.asyncIterator] and fall back to getReader().read().
“An async generator starts running when I call it.”
Calling it creates an iterator. The body starts when the consumer calls next(), usually through for await.
“Array.fromAsync is the same as Promise.all.”
They overlap for arrays of promises, but Array.fromAsync also pulls async iterables one value at a time. That is a different timing shape.
Practice: async iteration
5 EXERCISESWhat symbol-named method makes an object an async iterable?
The method key is Symbol.asyncIterator. for await asks for it before consuming the object.
Predict the console output. Write the lines separated by commas.
async function* letters() {
try {
yield "A";
yield "B";
} finally {
console.log("cleanup");
}
}
for await (const letter of letters()) {
console.log(letter);
break;
}The loop prints A, then break calls the generator's cleanup path, so finally prints cleanup.
What does the program print?
async function* names() {
yield "Ada";
yield "Lin";
}
const list = await Array.fromAsync(names());
console.log(list.join(" & "));async function* names() {
yield "Ada";
yield "Lin";
}
const list = await Array.fromAsync(names());
console.log(list.join(" & "));Array.fromAsync collects Ada and Lin, then join prints Ada & Lin.
This code logs promise objects instead of A and B. What loop should replace it?
const values = [Promise.resolve("A"), Promise.resolve("B")];
for (const value of values) {
console.log(value);
}for await (const value of values) {
console.log(value);
}Use for await...of when the iterable is sync but yields promises you want to unwrap one by one.
Sketch how you would load products from a paginated API where each response contains items and nextPage. Which tool from this lesson fits best?
async function* products(firstUrl) {
let url = firstUrl;
while (url) {
const page = await fetchJson(url);
yield* page.items;
url = page.nextPage;
}
}The generator hides pagination. UI code can for await products, render each one, and break if the user has seen enough.
Quiz: check your understanding
8 QUESTIONSRead the code carefully. The explanations are the important part, especially for the choices you do not pick.
Question 1 of 8Which method makes an object an async iterable?
Choose an answer to see the explanation.
Question 2 of 8What does an async iterator's
next()return?Choose an answer to see the explanation.
Question 3 of 8What does the early-break generator print?
Read the code, then predictasync function* letters() { try { yield "A"; yield "B"; } finally { console.log("cleanup"); } } for await (const letter of letters()) { console.log(letter); break; }Choose an answer to see the explanation.
Question 4 of 8Where may
for await...ofappear?Choose an answer to see the explanation.
Question 5 of 8What does this sync iterable of promises print?
Read the code, then predictconst values = [Promise.resolve(1), Promise.resolve(2), 3]; for await (const value of values) { console.log(value); }Choose an answer to see the explanation.
Question 6 of 8Which statement about async generators is true?
Choose an answer to see the explanation.
Question 7 of 8When is
Promise.allbetter thanfor await...of?Choose an answer to see the explanation.
Question 8 of 8What does
Array.fromAsyncdo?Choose an answer to see the explanation.
Key takeaways
- An async iterator has
Symbol.asyncIteratorand anext()method that returns a promise for{ value, done }. for await...ofawaits each value, works in async functions and module top level, and also accepts sync iterables.async function*is the usual way to write async iterables;finallyruns on normal finish, errors, or earlybreak.- Paginated APIs and streams are natural async-iteration use cases.
- Use
Array.fromAsyncto collect values, but usePromise.allfor independent parallel work.
One-liner.
Async iteration is the iterator protocol with waiting built into every request for the next value.
Up next: Generators for control flow, where generators send values both ways and become a foundation for coroutine patterns.