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
- How Symbol.asyncIterator works
- Write async generators with async function*
- Loop streams with for-await-of
- Paginate an API lazily
- Tell async from sync iterators
- Clean up with break and finally
📚 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
- 1️⃣ Using for-of on an async iterable
- 2️⃣ Forgetting for-await must be inside an async function
- 3️⃣ Mixing up the two symbols
- 4️⃣ Buffering an endless stream into an array
🎯 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
| Concept | Syntax |
|---|---|
| Async iterable hook | [Symbol.asyncIterator]() {} |
| Async generator | async function* g() {} |
| Produce a value | yield value; |
| Consume a stream | for await (const x of s) {} |
| Manual step | await it.next() |
📚 Keep Learning
- • MDN — for await...of
- • MDN — AsyncGenerator
- • MDN — Symbol.asyncIterator
🎉 Lesson Complete — Async Iterators!
- ✅ Async iterables expose Symbol.asyncIterator
- ✅ async function* is the easy way to build one
- ✅ for await (const x of s) consumes a stream lazily
- ✅ Perfect for pagination, file/line streaming, and live feeds
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
- Previous: Promise Combinators (all, race, allSettled, any)
- Next: AbortController & Cancelling Async Work — Cancel fetches and timers cleanly with AbortController and signals
- Quick reference: JavaScript cheat sheet
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.