Async Iterators & for-await-of

Reviewed & published by Brayan K

An async iterator is an object whose next() returns a promise of { value, done }, letting you pull values that arrive over time — one awaited step at a time — using the for-await-of loop.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

They power streaming: reading a file line by line, paginating an API, or consuming a live feed without ever loading everything into memory at once.

What You'll Learn in This Lesson

📚 Prerequisites: Make sure you know async/await and promises. Async iterators glue those together with looping.

📬 Real-World Analogy: A regular array is like a stack of mail already on your desk — all there, read it whenever. An async iterator is like checking your mailbox: each trip might involve a wait for the next letter to arrive, and you only hold one letter at a time. for-await-of is you walking to the mailbox over and over until there is nothing left.

1️⃣ The Protocol — Symbol.asyncIterator

An object is async iterable if it has a method keyed by Symbol.asyncIterator that returns an async iterator — an object with a next() method that returns a promise of { value, done }. The for-await-of loop awaits each of those promises for you.

Here it is written out by hand so you can see exactly what the loop is driving:

const range = {
  [Symbol.asyncIterator]() {
    let n = 1;
    return {
      next() {
        if (n > 3) return Promise.resolve({ value: undefined, done: true });
        // pretend each value needed an async wait
        return Promise.resolve({ value: n++, done: false });
      },
    };
  },
};

async function main() {
  for await (const x of range) {
    console.log("got:", x);
  }
  // ✅ Expected output:
  // got: 1
  // got: 2
  // got: 3
}

main();

2️⃣ Async Generators — the Easy Way

Writing the protocol by hand is tedious. async function* builds the whole thing for you: each yield produces a value, and because the function is async you can await between yields. Calling it returns a ready-to-loop async iterable.

This is the real streaming pattern — here a generator emits a tick every short delay, exactly how you might stream rows from a database cursor:

💡 Tip: The values are produced lazily. The generator is paused at each yield until the loop asks for the next one, so nothing runs ahead of what you consume.

async function* ticker(times) {
  for (let i = 1; i <= times; i++) {
    // a tiny wait stands in for real async work
    await new Promise((r) => setTimeout(r, 10));
    yield "tick " + i;
  }
}

async function main() {
  for await (const t of ticker(3)) {
    console.log(t);
  }
  // ✅ Expected output:
  // tick 1
  // tick 2
  // tick 3
}

main();

🎯 Your Turn #1 — Loop an Async Generator

Replace the blank with the correct loop keyword to consume the async generator.

async function* letters() {
  yield "a";
  yield "b";
  yield "c";
}

async function main() {
  // 👉 replace ___ so this awaits each value (hint: two words)
  for ___ (const ch of letters()) {
    console.log(ch);
  }
  // ✅ Expected output:
  // a
  // b
  // c
}

main();

3️⃣ Real Use — Paginating an API Lazily

The killer app for async generators is pagination. Wrap the "fetch a page, yield each item, advance the cursor" loop in an async generator and the caller just writes a single for-await-of — completely unaware of pages, cursors, or network waits.

Below, a Map-backed source stands in for the network so you can watch the same pattern stream every item across "pages" — with no DOM or fetch required:

// Fake paged backend: page -> { items, nextPage }
const pages = new Map([
  [1, { items: ["a", "b"], nextPage: 2 }],
  [2, { items: ["c", "d"], nextPage: 3 }],
  [3, { items: ["e"], nextPage: null }],
]);

// Stands in for: const res = await fetch(...); return res.json();
const getPage = (n) =>
  new Promise((resolve) => setTimeout(() => resolve(pages.get(n)), 5));

async function* fetchAllItems(startPage) {
  let next = startPage;
  while (next) {
    const page = await getPage(next);  // network wait
    for (const item of page.items) yield item;
    next = page.nextPage;
  }
}

async function main() {
  const collected = [];
  for await (const item of fetchAllItems(1)) {
    collected.push(item);
  }
  console.log("streamed:", collected.join(" "));
  // ✅ Expected output: streamed: a b c d e
}

main();

🎯 Your Turn #2 — Yield Values

Make the generator emit the squares 1, 4, 9 by replacing the blank with yield.

async function* squares(n) {
  for (let i = 1; i <= n; i++) {
    // 👉 replace ___ to emit each square value
    ___ i * i;
  }
}

async function main() {
  const out = [];
  for await (const sq of squares(3)) out.push(sq);
  console.log(out); // ✅ Expected output: [ 1, 4, 9 ]
}

main();

🧠 Quick Recall

Predict the output before revealing the answer.

1 false — next() returns a promise of { value, done }, and the first call yields 1 with done: false.

A TypeError — a plain for-of cannot drive an async iterable. You must use for await.

Symbol.asyncIterator — the sync version is Symbol.iterator.

Common Errors & Fixes

🎯 Mini Challenge — Async Filter

Build an async generator onlyEven that wraps another async iterable and yields only its even numbers. This is how you compose streaming pipelines.

async function* numbers(n) {
  for (let i = 1; i <= n; i++) {
    await new Promise((r) => setTimeout(r, 5));
    yield i;
  }
}

// Wraps a source stream and re-yields only even values
async function* onlyEven(source) {
  for await (const x of source) {
    if (x % 2 === 0) yield x;
  }
}

async function main() {
  const out = [];
  for await (const x of onlyEven(numbers(6))) {
    out.push(x);
  }
  console.log(out); // ✅ Expected output: [ 2, 4, 6 ]
}

main();

Quick Reference — Async Iteration

ConceptSyntax
Async iterable hook[Symbol.asyncIterator]() {}
Async generatorasync function* g() {}
Produce a valueyield value;
Consume a streamfor await (const x of s) {}
Manual stepawait it.next()

📚 Keep Learning

🎉 Lesson Complete — Async Iterators!

Practice quiz

What does an async iterator's next() return?

  • A value directly
  • An array
  • A promise of { value, done }
  • void

Answer: A promise of { value, done }. Unlike a sync iterator, an async iterator's next() returns a PROMISE of { value, done }.

Which loop consumes an async iterable?

  • for-await-of
  • for-of
  • forEach
  • while

Answer: for-await-of. for-await-of awaits each promise from next(); a plain for-of cannot drive an async iterable.

What happens if you use a plain for-of on an async iterable?

  • It works fine
  • It silently does nothing
  • It returns promises
  • A TypeError — it is not iterable that way

Answer: A TypeError — it is not iterable that way. A plain for-of cannot drive an async iterable and throws a TypeError.

Which symbol marks an object as async iterable?

  • Symbol.iterator
  • Symbol.asyncIterator
  • Symbol.for('async')
  • Symbol.next

Answer: Symbol.asyncIterator. The method keyed by Symbol.asyncIterator is what for-await-of looks for; the sync version is Symbol.iterator.

What does 'async function*' create?

  • An async generator (an async iterable)
  • An async function
  • A normal generator
  • A promise

Answer: An async generator (an async iterable). async function* builds an async generator, the easiest way to produce an async iterable.

Inside an async generator, which keyword produces each value?

  • return
  • await
  • yield
  • emit

Answer: yield. Each yield produces a value, and you can await between yields because the function is async.

Why stream with for-await-of instead of collecting everything into an array first?

  • It is the only way that compiles
  • Memory stays flat — each value can be released as the next arrives
  • Arrays cannot hold promises
  • It is slower but simpler

Answer: Memory stays flat — each value can be released as the next arrives. Processing each value as it arrives keeps memory flat even for huge or endless streams.

For await const r = await g().next() on 'async function* g(){ yield 1; yield 2; }', what is r.value and r.done?

  • 1 and true
  • undefined and true
  • 2 and false
  • 1 and false

Answer: 1 and false. The first next() yields value 1 with done: false.

for-await-of must appear inside what?

  • A class method
  • An async function
  • A generator
  • Any function works

Answer: An async function. Using for await outside an async function is a SyntaxError.

What is the killer use case for async generators highlighted in the lesson?

  • Sorting arrays
  • Math calculations
  • Lazily paginating an API one page at a time
  • DOM manipulation

Answer: Lazily paginating an API one page at a time. Wrapping fetch-a-page-and-yield-each-item in an async generator lets the caller write a single for-await-of, unaware of paging.

Continue this course

Frequently asked questions

What is the difference between a sync iterator and an async iterator?

A sync iterator's next() returns { value, done } immediately, so you loop it with a normal for-of. An async iterator's next() returns a PROMISE of { value, done }, so each step can wait for async work — and you loop it with for-await-of. The async version is what you want when the next value has to be fetched, read, or computed asynchronously.

What does async function* actually create?

It creates an async generator. Each yield produces a value, and because the function is async you can await inside it before yielding. Calling it returns an async iterable object that has a [Symbol.asyncIterator] method, so you can drive it with for-await-of. It is the easiest way to build a stream of values over time.

Why use for-await-of instead of collecting everything into an array first?

for-await-of processes each value as it arrives and lets the previous one be garbage-collected, so memory stays flat even for huge or endless streams. Collecting everything into an array first means waiting for all of it and holding all of it in memory at once — fine for small data, fatal for a 2GB log file or an infinite feed.

Can I use break and try/finally with for-await-of?

Yes. break stops the loop early and JavaScript automatically calls the iterator's return() method, which runs any finally block in the generator — perfect for closing a file or connection. Wrapping cleanup in try/finally inside the generator guarantees it runs whether the consumer finishes normally or breaks out early.

Related lessons