Iterators and Generators (incl. for-await-of)
TL;DR
An iterator is an object with a next() method that returns { value, done }. The iterable protocol says “this object has a [Symbol.iterator] method that returns an iterator.” Anything with that protocol works with for...of, spread, destructuring. Generators (function* + yield) are a syntax for creating iterators without writing the next() plumbing. Async iterators + for await...of handle streams (Node streams, ReadableStream, paginated APIs). The newer iterator helpers (ES2025) add map/filter/take/reduce directly on iterators — like array methods, but lazy.
Interview Q&A
Q: Iterable vs iterator — what’s the distinction?
A:
- Iterable: an object with
[Symbol.iterator]()that returns an iterator. - Iterator: an object with
next()returning{ value, done }.
Most built-ins (Array, Map, Set, String, NodeList, generators) are iterable. Calling obj[Symbol.iterator]() gives you the iterator.
const arr = [1, 2, 3];
const it = arr[Symbol.iterator]();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }
Q: Custom iterable — show me.
A:
class Range {
constructor(public start: number, public end: number) {}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next: () => current < end
? { value: current++, done: false }
: { value: undefined, done: true },
};
}
}
for (const n of new Range(1, 5)) console.log(n); // 1, 2, 3, 4
[...new Range(0, 3)]; // [0, 1, 2]
const [a, b] = new Range(10, 20); // a=10, b=11
Hand-rolling iterators is rare — use generators instead.
Q: Generators — what they buy you.
A: Same iterable + iterator, much less code:
function* range(start: number, end: number) {
for (let i = start; i < end; i++) yield i;
}
for (const n of range(1, 5)) console.log(n);
[...range(0, 3)]; // [0, 1, 2]
function* creates a generator function; calling it returns a generator object (which is both iterable and iterator). yield pauses execution; next() resumes.
Q: yield and yield*.
A:
function* outer() {
yield 1;
yield 2;
yield* inner(); // delegate to another iterable
yield 5;
}
function* inner() {
yield 3;
yield 4;
}
[...outer()]; // [1, 2, 3, 4, 5]
yield* flattens another iterable into the current generator. Useful for composing generators or yielding through built-ins (yield* arr).
Q: When use generators?
A: Lazy sequences:
- Infinite series —
function* naturals() { let i = 0; while (true) yield i++; }. - Streaming parsers — yield tokens as you parse a string.
- Tree traversal — yield nodes in order without building an array.
- Custom iteration for domain objects.
Avoid generators where simple array methods work — they’re slower and harder to read for short cases.
Q: Async iterators and for await...of.
A: Async iterators have [Symbol.asyncIterator]() returning an iterator whose next() returns a Promise<{ value, done }>. Use for await...of:
async function* fetchAllPages(url: string) {
let cursor: string | undefined;
do {
const res = await fetch(`${url}?cursor=${cursor ?? ""}`);
const { items, nextCursor } = await res.json();
for (const item of items) yield item;
cursor = nextCursor;
} while (cursor);
}
for await (const item of fetchAllPages("/api/users")) {
console.log(item);
}
Each iteration fetches the next page; consumer doesn’t see the pagination. Same pattern for streams, message queues, SSE.
async function* creates async generators — natural syntax for streamed work.
Q: Native async iterators in browser / Node.
A:
- Node streams —
fs.createReadStreametc. implement async iteration:for await (const chunk of fs.createReadStream("big.txt", "utf-8")) { process(chunk); } fetchbody (browser) —response.bodyis aReadableStream; async iteration works:for await (const chunk of response.body!) { process(chunk); }- Web events — Web Streams API + helpers.
For SSE consumption with POST (where EventSource can’t be used), for await over response.body is the natural pattern (see ../11_apis_data_fetching/08_server_sent_events.md).
Q: Iterator helpers — ES2025.
A: Methods directly on iterators, lazy by default:
function* naturals() { let i = 1; while (true) yield i++; }
const result = naturals()
.filter(n => n % 2 === 0)
.map(n => n * 10)
.take(5)
.toArray();
// [20, 40, 60, 80, 100]
Available methods: map, filter, take, drop, flatMap, reduce, toArray, forEach, some, every, find.
Key difference from array methods: lazy. .filter().map() doesn’t iterate the whole sequence — it pipelines. Perfect for infinite or large sequences where you only need a slice.
Available in Chrome/Edge 122+, Firefox 131+, Safari 18.4+, Node 22+. Polyfill via core-js or iterator-helpers-polyfill.
Q: Generators for cancellation / control flow.
A: Generators predate async/await and were the foundation for libraries like co.js + Redux Saga. The cooperative pause/resume is still useful for:
- State machines — each
yieldis a state. - Cancellable async — Saga’s
cancel()works because generators can be aborted atyield. - Coroutines — multiple generators sharing control.
For routine async, async/await is simpler. Reach for generators when the control flow is genuinely about pausing/resuming.
Q: return and throw on iterators.
A: Iterators may implement return(value) (early termination) and throw(error) (re-raise inside the iterator’s body):
function* counter() {
try {
let i = 0;
while (true) {
yield i++;
}
} finally {
console.log("cleanup");
}
}
const it = counter();
it.next(); // 0
it.next(); // 1
it.return(99); // cleanup logged, returns { value: 99, done: true }
for...of calls return() when breaking early (loop body break or throw). Use the finally in a generator for cleanup — for...of ensures it runs.
Gotchas / edge cases
- Generators are iterators and iterables —
gen[Symbol.iterator]()returnsgenitself. So you can both iterate them and callnext()directly. - Iterators are stateful —
for...ofon the same iterator twice yields nothing the second time (already done). - Spread
[...obj]requires an iterable —[...{}]throws “not iterable” because plain objects aren’t iterable by default. UseObject.entries(obj)to iterate. for...inis for object keys;for...ofis for iterables. Easy to mix up.- Async iterators in browsers were spotty until ~2022; modern is fine.
- Generators inside React render — don’t. React renders are pure; calling a generator function in render creates new state. Use in effects/handlers.
- TypeScript types — generator types:
Generator<TYield, TReturn, TNext>. Most cases only care aboutTYield.
What a senior is expected to say
- “Iterable + iterator protocol is what
for...of, spread, destructuring depend on. Generators are the syntax for writing iterators concisely.” - “Async generators +
for await...offor streamed data — pagination, server-sent events, file streams. Hides the iteration mechanics.” - “Iterator helpers (ES2025) bring lazy
map/filter/takedirectly on iterators. Pipelines large or infinite sequences without materializing arrays.” - “Generators for cooperative pausing/resuming — state machines, Redux Sagas. For routine async,
async/awaitis simpler.” - “
for...ofcallsreturn()on the iterator on early break — useful for cleanup infinally.”
Cross-references
- Async / Promises: ../03_javascript_core/async_js/
- SSE streaming with
for awaitover fetch body: ../11_apis_data_fetching/08_server_sent_events.md - Modern collection methods (companion features): 05_modern_collection_methods.md
Further reading
- MDN — Iteration protocols: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
- MDN —
function*: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function* - MDN —
for await...of: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of - TC39 — Iterator helpers: https://github.com/tc39/proposal-iterator-helpers