cf.completefrontendCode editorOpen lab
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 protocolRecognize an object whose next() returns a promise of { value, done }.
  • 02
    Consume async dataUse for await...of with async generators, streams, and sync iterables of promises.
  • 03
    Hide 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.

Real-life analogyAn async iterator is a WhatsApp chat

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

INTERACTIVE

A 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 }.

Sync and async iterator protocols
FeatureSync iteratorAsync iterator
Method keySymbol.iteratorSymbol.asyncIterator
next() returns{ value, done }A promise for { value, done }
Loopfor...offor await...of
Best forValues already availableValues 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.

Protocol lab: build the iterator by hand
Hand-written async iterablePop 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 sync iterable 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
statusRun the protocol examples.
Try it yourself

The object uses [Symbol.asyncIterator] and next(). The array example proves for await also accepts sync iterables and awaits each value.

Both examples run in this page; no external URL is used.
Where you can write it

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 THROUGH

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

Real-life analogyWaiting by the door

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 await loop
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: break calls cleanup through return()

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 a for await loop
Step 0 of 10Ready
Your turn: follow the blue line

Step through each await, yield, loop body, and cleanup moment.

Running in
  1. script
Next: line 10
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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;}
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose how the loop ends.

Changing the choice records a fresh run. Watch line 6: cleanup runs in both modes.

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

Errors travel through the pending next

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

INTERACTIVE

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

Real-life analogyBorrowing one library book at a time

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: hasMore or 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: break stops 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.

Pagination lab: stop before every page is fetched
Pagination generatorsPop 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;  }}
Request log1 entries
statusChoose settings, then run the fake API.
Try it yourself

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.

The fake API returns real 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

INTERACTIVE

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

Stream lab: chunks over time
Stream async iterationPop 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);
Stream output1 rows
statusFeature detection runs after mount.
Try it yourself

The stream is created in the page. Newer browsers can iterate it directly; older ones can still use a reader loop.

Feature detection happens after mount so server and browser rendering stay honest.

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

INTERACTIVE

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

Array.fromAsync lab
Collect async valuesPop 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(""));
Collected arrays1 rows
statusRun to compare collection shapes.
Try it yourself

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.

Choose the right loop
  • Read every page from an API that gives a nextPage token
  • Decode chunks from a ReadableStream as 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
Try it yourself
0 of 6 correct

Put each task under the tool that matches its data shape.

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

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

What symbol-named method makes an object an async iterable?

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

    Exercise 2 · PracticePredict cleanup

    Predict the console output. Write the lines separated by commas.

    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 yielded 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 logs promise objects instead of A and B. What loop should replace it?

        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 · ChallengeDesign a product loader

          Sketch how you would load products from a paginated API where each response contains items and nextPage. Which tool from this lesson fits best?

            Quiz: check your understanding

            8 QUESTIONS

            Read the code carefully. The explanations are the important part, especially for the choices you do not pick.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Which method makes an object an async iterable?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does an async iterator's next() return?

              Choose an answer to see the explanation.

            3. Question 3 of 8What does the early-break generator print?

              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 may for await...of appear?

              Choose an answer to see the explanation.

            5. Question 5 of 8What does this sync iterable 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 statement about async generators is true?

              Choose an answer to see the explanation.

            7. Question 7 of 8When is Promise.all better 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 async iterator has Symbol.asyncIterator and a next() method that returns a promise for { value, done }.
            • for await...of awaits 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; finally runs on normal finish, errors, or early break.
            • Paginated APIs and streams are natural async-iteration use cases.
            • Use Array.fromAsync to collect values, but use Promise.all for 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.

            CompleteFrontend Clear concepts. Working examples.