Iterators and Generators
An object is iterable if it implements a method at the special key Symbol.iterator that returns an iterator — an object with a .next() method returning { value, done }.
🎯 What you'll learn
- What Makes Something "Iterable"
- Built-in Iterables
- Building a Custom Iterable
- Iterable vs Array-Like
- What a Generator Is
1What Makes Something "Iterable"
An object is iterable if it implements a method at the special key Symbol.iterator that returns an iterator — an object with a .next() method returning { value, done }.
const arr = [10, 20, 30];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }This is the exact mechanism for...of, spread (...), and destructuring all rely on internally — anything implementing this protocol works with all three, automatically.
2Built-in Iterables
| Type | Iterable? |
|---|---|
| Array | ✅ |
| String | ✅ (iterates by Unicode code point, not raw UTF-16 code unit) |
| Map | ✅ (iterates [key, value] pairs) |
| Set | ✅ (iterates values) |
Plain object {} | ❌ not iterable by default |
arguments object | ✅ |
| NodeList (browser DOM) | ✅ |
for (const char of "hi") console.log(char); // "h", "i"
for (const [k, v] of new Map([["a", 1]])) console.log(k, v); // "a" 1
// for (const x of { a: 1 }) {} // TypeError: {a:1} is not iterable3Building a Custom Iterable
Implementing Symbol.iterator on your own object makes it work with for...of, spread, and destructuring:
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
}
}
for (const n of new Range(1, 5)) console.log(n); // 1, 2, 3, 4, 5
console.log([...new Range(1, 3)]); // [1, 2, 3] — spread works too
const [first, second] = new Range(1, 5); // destructuring works too4Iterable vs Array-Like
An array-like object has a length and numeric indices, but no Symbol.iterator — for...of and spread do not work on it directly.
function example() {
console.log(typeof arguments[Symbol.iterator]); // "function" — arguments IS iterable
}
const fakeArrayLike = { 0: "a", 1: "b", length: 2 }; // NOT iterable
// [...fakeArrayLike]; // TypeError
Array.from(fakeArrayLike); // ["a", "b"] — Array.from handles array-likes AND iterablesArray.from is the standard bridge for converting either array-likes or iterables into a real array.
⚠️ Common Pitfalls
- Assuming all objects with
lengthare iterable — array-likes needArray.fromor manual conversion; they don't work withfor...of/spread directly. - Manually calling
.next()and forgetting to check.done— reading.valueafterdone: truegivesundefined, easy to misinterpret as a real value ofundefined. - Re-using a "used up" iterator — once an iterator reaches
done: true, calling.next()again typically keeps returningdone: true; get a fresh iterator (or a freshfor...ofpass over the iterable) to iterate again.
1What a Generator Is
A generator function (function*) is a function that can pause and resume its own execution, automatically implementing the iterator protocol for you — no manual { value, done } objects required.
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i; // pause here, hand `i` out, wait to be resumed
}
}
const gen = range(1, 3);
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
for (const n of range(1, 3)) console.log(n); // 1, 2, 3 — generators ARE iterablesCalling a generator function does not run its body immediately — it returns a generator object. Execution only proceeds up to the next yield each time .next() is called.
2Rewriting the Custom Range Class from Part 1
class Range {
constructor(start, end) { this.start = start; this.end = end; }
*[Symbol.iterator]() { // generator method — no manual next()/done object needed
for (let i = this.start; i <= this.end; i++) yield i;
}
}
for (const n of new Range(1, 3)) console.log(n); // 1, 2, 3Generators are the standard, much simpler way to implement Symbol.iterator compared to hand-writing the protocol from Part 1.
3yield Can Receive Values Back
function* echo() {
while (true) {
const received = yield "waiting for input";
console.log("got:", received);
}
}
const gen = echo();
gen.next(); // starts the generator, runs to the first yield
gen.next("hello"); // resumes, `received` becomes "hello", logs "got: hello"The argument passed to .next(value) becomes the result of the yield expression that was paused — a two-way communication channel between caller and generator.
4yield* — Delegating to Another Iterable
function* inner() { yield 1; yield 2; }
function* outer() {
yield 0;
yield* inner(); // delegates — yields everything inner() yields, in order
yield 3;
}
console.log([...outer()]); // [0, 1, 2, 3]5Infinite Sequences (Lazy Evaluation)
Generators only compute values as they're requested — this makes infinite sequences practical:
function* naturalNumbers() {
let n = 1;
while (true) yield n++;
}
function take(iterable, count) {
const result = [];
for (const value of iterable) {
if (result.length >= count) break;
result.push(value);
}
return result;
}
take(naturalNumbers(), 5); // [1, 2, 3, 4, 5] — never actually generates beyond what's neededA plain array could never represent this — it would need infinite memory. A generator computes values lazily, one at a time.
6Generators for Error Handling
function* worker() {
try {
yield 1;
yield 2;
} catch (e) {
console.log("caught inside generator:", e.message);
}
}
const g = worker();
g.next();
g.throw(new Error("stop")); // injects an error at the paused yield pointUnder the HoodGenerators Are Real Suspended Execution Frames▾
A normal function's stack frame (see the Call Stack topic) is destroyed the instant it returns — its local variables simply cease to exist. A generator fundamentally cannot work this way, since it must resume exactly where it left off, with all of its local state intact, potentially long after the original call to the generator function.
The frame moves to the heap, not the stack
When you call a generator function, the engine does not run it on the normal call stack at all. Instead, it creates a generator object on the heap that holds a complete, suspendable execution frame: the current instruction pointer (exactly which point in the function body execution has paused at), every local variable's current value, and enough state to resume the underlying bytecode execution from that exact point.
function* counter() {
let count = 0; // this local variable lives inside the generator's HEAP-allocated frame,
while (true) { // not a normal stack frame — it survives between .next() calls
count += 1;
yield count;
}
}Each call to .next() briefly pushes the generator's saved frame back onto the call stack, runs until the next yield (or return, or the function ends), then pops it back off and stores the updated state back in the heap object — repeating this hand-off is what makes pause/resume possible at all.
This is why generators have closure-like memory implications
A paused generator — one that has been .next()-ed at least once but hasn't finished — keeps its entire local state reachable for as long as anything holds a reference to the generator object, exactly like an active closure's Context (see the Closures topic). An infinite generator that's created but never fully consumed, and never garbage collected because something still references it, keeps its (possibly growing) internal state alive indefinitely — the practical cost of the laziness described in section 5.
The pre-native implementation: a state machine
Before engines implemented generators natively, tools like Babel's "regenerator" transform simulated this same pause/resume behavior using ordinary functions: the generator body was compiled into a single function containing a loop and a switch statement, where each case represented the code between two yield points, and a saved "which case am I on" variable played the role of the instruction pointer. Understanding this transform is a useful mental model even now: a generator is conceptually "a function that remembers which case to jump into next," whether that's implemented via a literal switch statement (transpiled) or a native suspended frame (modern engines) — same idea, different execution layer.
⚠️ Common Pitfalls
- Forgetting generators are lazy — nothing runs until
.next()is called; a generator function's body does not execute at all just by calling it. - Infinite generators without a break condition in the consumer —
for...ofovernaturalNumbers()with nobreakwill loop forever. - Reusing an exhausted generator — once done, a generator object cannot be restarted; call the generator function again to get a fresh one.
- Confusing
yieldwithreturn—return valueinside a generator sets{ value, done: true }on that final.next()call and ends iteration;for...ofdoes NOT include this final returned value in its iteration (onlydone: falsevalues are yielded tofor...of).
📋 Quick Reference
function* gen() { yield x; } → generator function
gen().next() → { value, done }
yield* otherIterable → delegate to another iterable
for (const v of gen()) {} → generators are iterables🎤 Interview Q&A
Q1What is the iteration protocol?core▾
An object is iterable if it has [Symbol.iterator]() returning an iterator with next() that returns { value, done }. for…of, spread, destructuring and Array.from use it.
- Which built-ins are iterable? Arrays, strings, Maps, Sets, arguments, NodeLists.
Q2What are generators?core▾
Functions declared with function* that can pause at yield and resume later, returning an iterator. They make lazy sequences and custom iterables easy.
- Infinite sequence example?
function* ids() { let i = 0; while (true) yield i++; }.
Q3What are async iterators and for await?core▾
Iterators whose next() returns promises; for await (const x of source) consumes them. Used for streams, paginated APIs and event sources. async function* creates them.
- Node streams? Readable streams are async iterable.
Q4How can generators implement lazy pipelines?senior▾
Chain generator functions (take(filter(map(source)))) so items are processed one at a time on demand, without intermediate arrays, enabling infinite or very large sequences.
- Iterator helpers? Native
.map/.filter/.takeon iterators.
Q5What are return() and throw() on iterators?core▾
return() ends iteration early (called automatically by break in for…of, letting generators clean up in finally); throw() injects an error at the paused yield.
- Why does it matter? Resource cleanup when consumers stop early.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.