completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

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.

By the end you can
  • 01
    Read the rulesSpot an object whose next() gives back a promise for { value, done }.
  • 02
    Loop over values that arrive laterUse for await...of with async generators, streams, and arrays of promises.
  • 03
    Hide the messy partsPut page loading and cleanup inside an async generator, so the loop that uses it can break safely.

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.

Real-life analogyWaiting for messages in a chat

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...of loop, 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()

INTERACTIVE

A 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 }. value is the next value. done is true when there are no more.

The async rules change only two things:

  • The method is called Symbol.asyncIterator instead of Symbol.iterator.
  • next() gives back a promise. The promise then gives you the { value, done } result.
The first mailbox.next() call gives back a promise straight away. Then the promise gives the result: value invoice, done false.mailbox.next()you askpromiseyou get this firstthenvalue: "invoice"done: false
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.

Normal and async loop rules, side by side
What you compareNormal loop rulesAsync 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.

Rules lab: an object that gives values later
An object that follows the async rules by handPop out in the code editor (opens in a new tab)JavaScript
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);}
A normal array of promisesPop out in the code editor (opens in a new tab)JavaScript
const tasks = [  Promise.resolve("wake"),  Promise.resolve("brew"),  "sip",]; for await (const task of tasks) {  console.log(task);}
Loop output1 rows
statusPress the button to run both loops.
Try it yourself

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.

Both examples run right here in this page.
Where you can write 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 THROUGH

for await...of is the loop that uses async values. You don't call next() yourself. The loop does it for you, every turn:

  1. It calls next() and gets a promise.
  2. It waits until the promise gives the result.
  3. It runs the loop body with the value.
  4. It goes back to step 1, until done is true.
The for await loop over letters(). Step 1, it calls next(). Step 2, it waits for the promise. Step 3, the body runs with letter set to A. Step 4, it goes back and asks again, for B and then C.for await (const letter of letters())1. asknext()2. waitpromise3. run the bodyletter = "A"4. ask again: B, then C
Each turn of the loop asks, waits, then runs the body. The loop asks for the next letter only after the body has finished.

So the loop body runs for one value at a time, in order. The next turn starts only when the body has finished.

Real-life analogyWaiting by the door

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 await loop
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: break makes the loop call return()

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:

  • yield gives out one value and pauses the function there.
  • finally marks 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.

Stop an async generator by calling return()Pop out in the code editor (opens in a new tab)JavaScript
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 leaves the loop early. The loop then calls return() on the generator for you. return() runs the finally block, which prints cleanup.breakloop stops earlyreturn()called for youfinallyprints "cleanup"
Leaving with 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 a for await loop
Step 0 of 10Ready
Your turn: follow the blue line

Step through each wait, each yield, each turn of the loop body, and the cleanup.

Running in
  1. script
Next: line 10
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;}
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose how the loop ends.

Each choice records a new run. Watch line 7: the cleanup runs with both choices.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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 a for await loop.
  • Inside, await waits for data. yield gives 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.

Catch an error that the generator throwsPop out in the code editor (opens in a new tab)JavaScript
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.

Errors arrive as a failed next()

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

INTERACTIVE

Many 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.

Real-life analogyBorrowing one library book at a time

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 hasMore is true
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: break stops any more page loads

The lab code has two async generators:

  • pages() loads one page at a time. It stops when a page says hasMore is false.
  • 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.

Page size 3, stop after 5 items. Page 1 holds book-1 to book-3, and all three are used. Page 2 holds book-4 to book-6. book-4 and book-5 are used, then the loop breaks. Page 3 is never fetched.Stop after 5 itemsbook-1 to 3page 1all 3 usedbook-4 to 6page 2book-4, book-5 usedthen breakpage 3never fetched
With a page size of 3, the loop needs only pages 1 and 2 to get 5 items. It breaks after book-5, so page 3 is never fetched.
Pages lab: stop before every page is loaded
Two generators: one for pages, one for itemsPop out in the code editor (opens in a new tab)JavaScript
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;  }}
What happened1 entries
statusChoose the settings, then run the pretend server.
Try it yourself

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.

The pretend server is a function in this page. It waits a real amount of time, then gives back a real 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

INTERACTIVE

A 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 says done.

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.

Stream lab: read the chunks
Loop over a stream with for awaitPop out in the code editor (opens in a new tab)JavaScript
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);
What the stream gave1 rows
statusThe check runs once the page has loaded.
Try it yourself

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 browser check runs after the page loads, so the page looks the same on the server and in the browser at first.

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

INTERACTIVE

Sometimes 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 await to 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.

Array.fromAsync lab: collect the values
Collect values into arraysPop out in the code editor (opens in a new tab)JavaScript
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(""));
The arrays1 rows
statusPress the button to collect the values.
Try it yourself

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.

Promises made before the loop have already startedPop out in the code editor (opens in a new tab)JavaScript
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 can change each value too

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.

Choose the right loop
  • Read every page from a server, where each page gives the nextPage address
  • Turn chunks from a ReadableStream into 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
Try it yourself
0 of 6 correct

Put each task under the loop that fits it best.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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 EXERCISES
Exercise 1 · Warm-upName the method

Which method, named with a symbol, lets for await loop over an object?

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePredict the cleanup

    What does the console show? Write the lines with commas between them.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    async function* letters() {
      try {
        yield "A";
        yield "B";
      } finally {
        console.log("cleanup");
      }
    }
    
    for await (const letter of letters()) {
      console.log(letter);
      break;
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCollect the names

      What does the program print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      async function* names() {
        yield "Ada";
        yield "Lin";
      }
      const list = await Array.fromAsync(names());
      console.log(list.join(" & "));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the loop bug

        This code prints promise objects instead of A and B. Which loop should it use instead?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const values = [Promise.resolve("A"), Promise.resolve("B")];
        for (const value of values) {
          console.log(value);
        }
          Exercise 5 · ChallengePlan a product loader

          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?

            Exercise 6 · PracticeCatch the generator's error

            What does the console show? Write the lines with commas between them.

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            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);
            }

            Answer, then press Check. Spacing and letter case don’t matter.

              Quiz: check your understanding

              8 QUESTIONS

              Read each code example carefully before you choose. Every choice has an explanation, so read the ones you didn't pick too.

              Lesson quiz · 8 questionsScore: first tries count
              1. Question 1 of 8Which method lets for await loop over an object?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does next() give back in the async loop rules?

                Choose an answer to see the explanation.

              3. Question 3 of 8What does this code print when the loop uses break?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                async 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.

              4. Question 4 of 8Where can you write for await...of?

                Choose an answer to see the explanation.

              5. Question 5 of 8What does this loop over an array of promises print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const values = [Promise.resolve(1), Promise.resolve(2), 3];
                for await (const value of values) {
                  console.log(value);
                }

                Choose an answer to see the explanation.

              6. Question 6 of 8Which sentence about async generators is true?

                Choose an answer to see the explanation.

              7. Question 7 of 8When is Promise.all a better choice than for await...of?

                Choose an answer to see the explanation.

              8. Question 8 of 8What does Array.fromAsync do?

                Choose an answer to see the explanation.

              Key takeaways

              • An object you can loop over with for await has a Symbol.asyncIterator method. Its next() gives back a promise for { value, done }.
              • for await...of waits for each value in turn. You can use it inside async functions 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. Its finally block runs when it ends normally, when it throws, and when the loop uses break.
              • Loading pages from a server and reading streams are two common uses.
              • Use Array.fromAsync to collect all the values into an array. Use Promise.all when 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.

              CompleteFrontend Clear concepts. Working examples.