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 rulesSpot an object whose
next()gives back a promise for{ value, done }. - 02Loop over values that arrive laterUse
for await...ofwith async generators, streams, and arrays of promises. - 03Hide the messy partsPut page loading and cleanup inside an async generator, so the loop that uses it can
breaksafely.
Values that arrive later
Looping over an array is easy. Every value is already there, so the loop can take them one after another.
Many lists of values are not like that. The values come one at a time, and you have to wait for each one:
- A server sends page 1 of results, then later page 2.
- A download arrives in small pieces. Each piece is called a chunk.
- A chat gets a new message only when someone sends it.
Async iteration is how JavaScript loops over values that arrive later. ("Async" means "it can wait". "Iteration" means "going through values one by one".) Your code asks for the next value. It gets a promise: an object that gives you the value once it is ready. The loop waits for that promise, uses the value, then asks again.
Scrolling through old messages in a chat app is like a normal loop. They are all on your screen already. Waiting for a friend's reply is different. You wait. A new message arrives. You read it. Then you wait for the next one.
- In real life: The next message arrives later
- In JavaScript: The next value is ready later
- In real life: You check for a new message
- In JavaScript: The loop calls
next() - In real life: You wait until it arrives
- In JavaScript:
next()gives back a promise - In real life: The chat has ended
- In JavaScript: The result says
done: true
In this lesson you will:
- build an object that gives values later, by hand;
- step through a
for await...ofloop, line by line; - load pages from a pretend server, one page at a time;
- read a real stream of chunks in this page;
- collect all the values into an array with
Array.fromAsync; - pick the right kind of loop for everyday tasks.
The rules: Symbol.asyncIterator and next()
INTERACTIVEA protocol is a set of rules that objects agree to follow. You met the normal loop rules in the Iteration protocols lesson. Here is a quick reminder:
- An iterable is an object you can loop over, like an array. It has a method called
Symbol.iterator. - That method gives back an iterator: an object with a
next()method. - Each
next()call gives back a result,{ value, done }.valueis the next value.doneistruewhen there are no more.
The async rules change only two things:
- The method is called
Symbol.asyncIteratorinstead ofSymbol.iterator. next()gives back a promise. The promise then gives you the{ value, done }result.
next() always answers with a promise first, even when the value is ready. Then the promise gives you the result, { value, done }.The table puts the two sets of rules side by side. Read each row across: the normal version first, then the async version.
| What you compare | Normal loop rules | Async loop rules |
|---|---|---|
| What is the method called? | It is Symbol.iterator. | It is Symbol.asyncIterator. |
What does next() give back? | It gives { value, done } right away. | It gives a promise. The promise then gives { value, done }. |
| Which loop uses it? | for...of uses it. | for await...of uses it. |
| When is it a good fit? | The values are all there already. | The values arrive later, one by one. |
The lab below follows the rules by hand, with no shortcuts. The mailbox object holds two letters. Its next() method gives back a promise each time.
The second example shows one more thing. for await...of can also loop over a normal array. It waits for each promise in the array before the loop body runs.
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 mailbox object has a [Symbol.asyncIterator] method and a next() method that gives back a promise. The array example shows that for await also loops over a normal array and waits for each promise in it.
You can write for await...of in two places: inside an async function, or at the top level of a module (a file loaded with type="module" or import). A normal function or an old-style script can't wait like this.
for await...of: ask, wait, use, repeat
STEP THROUGHfor await...of is the loop that uses async values. You don't call next() yourself. The loop does it for you, every turn:
- It calls
next()and gets a promise. - It waits until the promise gives the result.
- It runs the loop body with the value.
- It goes back to step 1, until
doneistrue.
So the loop body runs for one value at a time, in order. The next turn starts only when the body has finished.
You wait by the door for the post. A letter drops in. You open it and read it. Then you wait for the next one. You can't read tomorrow's letter today. If you want to stop early, you put up a "no more mail" sign, so the post stops coming.
- In real life: Stand by the door
- In JavaScript: Start the
for awaitloop - In real life: Open one letter when it drops in
- In JavaScript: Run the loop body for one value
- In real life: Wait for the next letter
- In JavaScript: Wait for the next
next()promise - In real life: Put up a "no more mail" sign
- In JavaScript:
breakmakes the loop callreturn()
The values in the example below come from an async generator. It is a function written as async function* that gives out values one at a time. (The next section explains it in full.) Two words you need now:
yieldgives out one value and pauses the function there.finallymarks a block of code that always runs when the function stops, for any reason. It is a good place for cleanup, such as closing a connection.
Every async generator also has a return() method. Calling it stops the generator for good. The code below calls it by hand.
async function* orders() {
try {
yield "first";
yield "second";
} finally {
console.log("closed");
}
}
const iterator = orders();
console.log((await iterator.next()).value);
await iterator.return();Line 10 makes the generator. Line 11 calls next() once, waits, and prints first. Line 12 calls return(). The generator stops, and its finally block prints closed. second is never given out.
You rarely call return() yourself. When you leave a for await loop early with break, the loop calls return() for you.
break does not skip the cleanup. The loop calls return() for you, and the generator runs its finally block.Step through the run below. Choose whether the loop takes all three letters or breaks after the first one. Watch line 7: the cleanup line runs in both cases, because the generator has a finally block.
Step through each wait, each yield, each turn of the loop body, and the cleanup.
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 next() by hand, like the mailbox, shows how the rules work. But most code uses an async generator instead. You write it as async function*: the word async, the word function, and a star.
Here is what happens when you use one:
- Calling the function does not run its code yet. It only gives you an object that follows the async rules.
- The code starts when someone asks for the first value, with
next()or afor awaitloop. - Inside,
awaitwaits for data.yieldgives out one value. - After a
yield, the code pauses. It goes on only when the next value is asked for.
What if code calls next() several times without waiting for the answers? The generator puts the requests in a line. It answers them one by one, in order. It never runs two parts of its code at the same time.
A generator can also fail. In the code below, the generator gives out one value and then throws an error.
async function* statuses() {
yield "paid";
throw new Error("payment service unavailable");
}
try {
for await (const status of statuses()) {
console.log(status);
}
} catch (error) {
console.log(error.message);
}The first turn gets paid, so line 8 prints it. On the next turn, line 3 throws an error. The loop stops. Line 11, inside the catch block, prints the error's message.
Notice where the try is. It wraps the whole loop, not just the body. The error comes from the generator, so a try inside the body would not catch it.
If an async generator throws, the promise from next() fails (it rejects). A for await loop turns that into an error at the loop line. A try...catch around the loop can catch it.
Loading a list one page at a time
INTERACTIVEMany servers don't send a long list all at once. They send it in pages: page 1 has the first few items, page 2 the next few, and so on. This is called pagination.
Each page also says whether there is another page. In the code below, that is hasMore. You only know if you need page 2 after page 1 arrives. So the pages must be loaded one after another. An async generator fits this well.
At a library you borrow one book. You ask the librarian if another one is ready. If yes, you borrow it. When you have read enough, you stop asking.
- In real life: Borrow the first book
- In JavaScript: Load page 1
- In real life: The librarian says another book is ready
- In JavaScript: The page says
hasMoreistrue - In real life: Ask for the next book
- In JavaScript: The loop asks
items()for the next item - In real life: Stop borrowing
- In JavaScript:
breakstops any more page loads
The lab code has two async generators:
pages()loads one page at a time. It stops when a page sayshasMoreisfalse.items()loops over those pages and gives out each item on its own.yield*gives out every item of a list, one by one.
The code that uses items() sees one item at a time. It never has to think about pages.
book-5, so page 3 is never fetched.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 asks for the next page only when the loop needs more items. If the loop uses break, the finally block runs, and the later pages are never asked for.
Response object.Try stopping after fewer items than the total. The request log shows which pages were never loaded. The cleanup row shows that the finally block still ran.
Streams: a download in chunks
INTERACTIVEA stream is data that arrives in chunks over time. In the browser, a stream is a ReadableStream object. For example, the body of a fetch response is a stream.
There are two ways to read the chunks:
- Newer browsers let you loop over a stream with
for await...of. - Older browsers need a reader: you call
reader.read()again and again until it saysdone.
So real code should first check which way the browser supports. This check is called feature detection.
In the code, each chunk is raw bytes. TextEncoder turns text into bytes, and TextDecoder turns bytes back into text.
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 made in this page. Newer browsers can loop over it with for await. Older browsers read it with a reader.read() loop instead.
The lab makes a stream inside this page. When the page loads, it checks whether your browser can loop over a stream directly. Then it shows which way it used to read the chunks. For download progress bars and more, see the Streams & progress lesson.
Array.fromAsync: put all the values in an array
INTERACTIVESometimes you don't want to handle each value as it arrives. You just want all of them in one array. Array.fromAsync() does that:
- Give it an async generator, and it collects every value the generator gives out.
- Give it a normal array of promises, and it waits for each promise and collects the results.
- Either way, it gives back a promise. Use
awaitto get the finished array.
Array.fromAsync works in current browsers and in Node 22 and later. If you must support older ones, check that it exists first.
The lab code uses a helper, wait(value, ms). It waits ms milliseconds, then gives back value.
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(""));From the generator, Array.fromAsync gets one value at a time: A starts and is done before B starts. The array of promises is different. Making the array already started all three tasks, so Array.fromAsync and Promise.all just wait for results that are on their way.
Array.fromAsync works in current browsers and in Node 22 and later. For older ones, check that it exists first.When does each task start? The code below makes the promises first, then loops over them. step() records when each task starts and when it is done.
const events = [];
async function step(name) {
events.push("start " + name);
await Promise.resolve();
events.push("done " + name);
return name;
}
const tasks = [step("A"), step("B")];
for await (const task of tasks) console.log(task);
console.log(events.join(" | "));Line 9 calls step("A") and step("B") to build the array. Both tasks start right there, before the loop begins. Line 10 then prints A and B, in array order. Line 11 prints start A | start B | done A | done B: both had started before either one was done.
An async generator is different. It starts the next task only when the loop asks for the next value. When tasks don't depend on each other, starting them all together is faster. The Promise combinators lesson and the Sequential vs parallel lesson explain Promise.all, which does that.
Array.fromAsync takes an optional second argument: a function that changes each value. It waits for that function too. With an async generator, it still handles one value at a time. If the changes don't depend on each other, use Promise.all so they can all run together.
Where you will use this
Use async iteration when values arrive one by one and you want to handle each as it comes. Common cases in websites:
- loading search results page by page;
- reading a response from fetch chunk by chunk;
- handling a large uploaded file in pieces;
- turning live chat messages into a loop.
It is not always the right tool. Sort each task below by the simplest loop that does the job.
- Read every page from a server, where each page gives the
nextPageaddress - Turn chunks from a
ReadableStreaminto text as they arrive - Load 20 profile pictures that don't depend on each other
- Load three settings files before showing the page
- Add a class to each element that is already on the page
- Add up numbers that are already in an array
Put each task under the loop that fits it best.
Common misconceptions
"for await runs all the promises at the same time."
No. It waits for one value at a time, in order. When tasks don't depend on each other, use Promise.all so they run together.
"break only stops the loop body."
break also makes the loop call the generator's return() method. That runs the generator's finally block, so cleanup still happens.
"You can loop over any stream with for await."
Not in every browser version. First check that ReadableStream.prototype[Symbol.asyncIterator] exists. If it doesn't, read with getReader().read() instead.
"An async generator runs its code as soon as I call it."
No. Calling it only makes the generator object. Its code starts when something calls next(), usually a for await loop.
"Array.fromAsync is the same as Promise.all."
For an array of promises, they give the same array. But with an async generator, Array.fromAsync asks for one value, waits, then asks for the next. So the tasks run one after another, not together.
Practice: async iteration
5 EXERCISESWhich method, named with a symbol, lets for await loop over an object?
The method is called Symbol.asyncIterator. for await looks for it first, before it starts the loop.
What does the console show? Write the lines with commas between them.
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 makes the loop call return(), so the finally block 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 into an array. join turns it into Ada & Lin.
This code prints promise objects instead of A and B. Which loop should it use instead?
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. It waits for each promise in the array and gives the body the result.
A server sends products in pages. Each page has items (the products) and nextPage (the address of the next page, or nothing on the last page). How would you load the products? 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 handles the pages. The page code can for await over products(), show each product, and break when the user has seen enough.
What does the console show? Write the lines with commas between them.
async function* statuses() {
yield "paid";
throw new Error("payment service unavailable");
}
try {
for await (const status of statuses()) console.log(status);
} catch (error) {
console.log(error.message);
}async function* statuses() {
yield "paid";
throw new Error("payment service unavailable");
}
try {
for await (const status of statuses()) console.log(status);
} catch (error) {
console.log(error.message);
}The loop prints paid. On the next turn the generator throws. The catch around the loop prints payment service unavailable.
Quiz: check your understanding
8 QUESTIONSRead each code example carefully before you choose. Every choice has an explanation, so read the ones you didn't pick too.
Question 1 of 8Which method lets
for awaitloop over an object?Choose an answer to see the explanation.
Question 2 of 8What does
next()give back in the async loop rules?Choose an answer to see the explanation.
Question 3 of 8What does this code print when the loop uses
break?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 can you write
for await...of?Choose an answer to see the explanation.
Question 5 of 8What does this loop over an array 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 sentence about async generators is true?
Choose an answer to see the explanation.
Question 7 of 8When is
Promise.alla better choice 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 object you can loop over with
for awaithas aSymbol.asyncIteratormethod. Itsnext()gives back a promise for{ value, done }. for await...ofwaits for each value in turn. You can use it insideasyncfunctions and at the top level of a module. It also works on normal arrays of promises.async function*is the usual way to make values that arrive later. Itsfinallyblock runs when it ends normally, when it throws, and when the loop usesbreak.- Loading pages from a server and reading streams are two common uses.
- Use
Array.fromAsyncto collect all the values into an array. UsePromise.allwhen tasks don't depend on each other and can run together.
One-liner.
Async iteration is a loop that waits for each next value before it uses it.
Up next: Generators for control flow. You will send values both ways between a generator and the code that runs it.