Iterators and Generators
|
This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve. This section’s bibliography lists the reference material consulted while preparing these pages. |
for…of, the spread operator (…), and destructuring all work on arrays, strings, Set, and Map
"for free," but none of that is a special case baked into the language for those specific types — it is all
built on one general-purpose protocol that any object can opt into. This page explains that protocol from the
ground up: how for…of actually drives an object step by step, how to make a custom class participate in it,
and how generator functions (function*/yield) give you a dramatically shorter way to write an iterator than
implementing the protocol by hand.
The Iterator Protocol
Three distinct roles are involved in every iteration, and it helps to keep them separate:
-
An iterable is any object with a method named
Symbol.iterator(a well-known symbol, not a string) that returns an iterator. Arrays, strings,Set, andMapare all iterables in this sense. -
An iterator is any object with a
next()method. Each call advances the iteration by one step and returns an iteration result. -
An iteration result is a plain object with a
valueproperty (the item produced by this step) and adoneboolean (trueonce there is nothing left to produce).
for…of is syntactic sugar over calling [Symbol.iterator]() once to obtain an iterator, then calling
next() repeatedly until done is true. Written out longhand, without the sugar:
let iterable = [10, 20, 30];
let iterator = iterable[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
console.log(result.value); // 10, then 20, then 30
result = iterator.next();
}
The diagram below shows that same request/response cycle from the caller’s perspective — for…of (or the
spread operator, or destructuring) is always the one driving, and the iterator is always the one responding:
The built-in iterator types are themselves iterable — their Symbol.iterator method just returns
this. That is what lets a partially consumed iterator be resumed with another for…of or spread: let
iter = [1, 2, 3][Symbol.iterator](); iter.next(); […iter] yields [2, 3], picking up exactly where the
first next() call left off.
|
Because a Map iterates [key, value] pairs, and those pairs destructure naturally in a for…of loop’s
declaration, iterating a map’s entries reads almost like a dedicated syntax even though it is just the general
protocol at work:
let scores = new Map([["alice", 92], ["bob", 81]]);
for (const [name, score] of scores) {
console.log(name, score);
}
Writing a Custom Iterable
Any class can opt into for…of, spreading, and destructuring by implementing a [Symbol.iterator]() method
that returns an object with a next() method. The classic example is a numeric range that does not store its
members as an array — it computes each one on demand:
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
[Symbol.iterator]() {
// Each call must return an independent iterator, so the cursor
// lives in a variable captured by this closure, not on `this`.
let next = Math.ceil(this.from);
let last = this.to;
return {
next() {
return next <= last ? { value: next++, done: false } : { value: undefined, done: true };
},
// Making the iterator itself iterable is a small convenience
// that lets it be resumed directly with for...of or spread.
[Symbol.iterator]() {
return this;
},
};
}
}
[...new Range(1, 5)]; // => [1, 2, 3, 4, 5]
for (const n of new Range(1, 3)) console.log(n); // 1, then 2, then 3
Tracking the cursor (next) in a local variable rather than a property on Range matters: two independent
for…of loops over the same Range instance must not interfere with each other, and each call to
[Symbol.iterator]() returns a fresh closure with its own cursor.
The same pattern — return an object with a next() method that either produces a value or reports done — also works for iterables that filter or transform another iterable lazily, without ever materializing an
intermediate array:
function filter(iterable, predicate) {
let iterator = iterable[Symbol.iterator]();
return {
[Symbol.iterator]() {
return this;
},
next() {
for (;;) {
let item = iterator.next();
if (item.done || predicate(item.value)) return item;
}
},
};
}
[...filter(new Range(1, 10), (n) => n % 2 === 0)]; // => [2, 4, 6, 8, 10]
Laziness like this is the main payoff of implementing the protocol directly rather than always building an
array up front: filter() above only ever asks its source for the next value when its own caller asks it for
one, so an infinite or very large source can be filtered without ever holding more than one element in memory
at a time.
return(): cleaning up an interrupted iteration
for…of does not always run to completion — a break, an early return, or an uncaught exception can all
stop it partway through. If the iterable held onto a resource (an open file handle, a database cursor) that
needs closing, that cleanup must not depend on the iteration finishing normally. For exactly this case, an
iterator may implement an optional return() method: whenever iteration stops before next() reports
done: true, the interpreter checks for a return() method and, if one exists, calls it with no arguments so
the iterator gets a chance to release whatever it was holding:
function readLines(source) {
let connection = source.openConnection();
return {
[Symbol.iterator]() {
return this;
},
next() {
let line = connection.readLine();
return line === null ? { value: undefined, done: true } : { value: line, done: false };
},
return(value) {
connection.close(); // runs even if the loop exits early
return { value, done: true };
},
};
}
for (const line of readLines(logFile)) {
if (line === "STOP") break; // triggers return(), closing the connection
console.log(line);
}
return() must itself return an iteration result object — its value/done properties are ignored, but
returning a non-object is an error.
Generator Functions
Implementing Symbol.iterator, a next() method, and the { value, done } bookkeeping by hand works, but it
is a lot of ceremony for what is conceptually a simple idea: "produce these values, one at a time." Generator
functions — declared with function* instead of function — exist to remove that ceremony. Calling a
generator function does not run its body; it returns a generator object, which is both an iterator and an
iterable. Each call to that generator’s next() runs the function body from wherever it last stopped, up to
the next yield expression, and the yielded value becomes the value of that next() call’s result:
function* oneDigitPrimes() {
yield 2;
yield 3;
yield 5;
yield 7;
}
let primes = oneDigitPrimes(); // body has not run yet
primes.next().value; // => 2
primes.next().value; // => 3
[...oneDigitPrimes()]; // => [2, 3, 5, 7]
for (const p of oneDigitPrimes()) console.log(p); // 2, 3, 5, 7
The can appear on a function expression, and on a method in a class body or object literal (using the same
shorthand method syntax as a regular method, just with before the name):
const seq = function* (from, to) {
for (let i = from; i <= to; i++) yield i;
};
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
// A generator-based Symbol.iterator is far shorter than the
// hand-written version shown earlier in this page.
*[Symbol.iterator]() {
for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
}
}
There is no arrow-function form of a generator — const g = () ⇒ { … } is not valid syntax.
A generator must be declared with function, a function* expression, or the *methodName() shorthand.
|
A generator’s `yield`s do not have to be a fixed, hardcoded list — they can come from a loop or from real computation, including an unbounded one, which is where generators start to earn their keep:
function* fibonacciSequence() {
let a = 0, b = 1;
for (;;) {
yield b;
[a, b] = [b, a + b];
}
}
// Safe: for...of only pulls as many values as the loop body consumes.
function nthFibonacci(n) {
for (const f of fibonacciSequence()) {
if (n-- <= 0) return f;
}
}
nthFibonacci(10); // => 55
Because a generator only computes a value when next() actually asks for one, fibonacciSequence() above
never has to decide in advance how many terms to produce — spreading it ([…fibonacciSequence()]) would
run forever, but consuming it through a bounded for…of (as nthFibonacci does) is perfectly safe. A
take() generator makes that bound reusable instead of ad hoc:
function* take(n, iterable) {
let iterator = iterable[Symbol.iterator]();
while (n-- > 0) {
let next = iterator.next();
if (next.done) return;
yield next.value;
}
}
[...take(5, fibonacciSequence())]; // => [1, 1, 2, 3, 5]
Delegating with yield*
A generator often needs to yield every value of some other iterable in turn — forwarding, rather than producing values itself. Written naively, that is a nested loop:
function* sequence(...iterables) {
for (const iterable of iterables) {
for (const item of iterable) {
yield item;
}
}
}
yield* collapses the inner loop: it iterates the given iterable itself and yields each of its values in
turn, so the generator body only has to say what it is delegating to:
function* sequence(...iterables) {
for (const iterable of iterables) {
yield* iterable;
}
}
[...sequence("ab", oneDigitPrimes())]; // => ["a", "b", 2, 3, 5, 7]
yield* accepts any iterable — including another generator — which is what makes recursive generators
possible, e.g. flattening a tree structure by having each node’s generator yield* into its children’s
generators. yield and yield* are only legal directly inside a function* body: reaching for Array.prototype.forEach()
with a yield* inside its callback does not work, because that callback is an ordinary function, not a
generator, even though it is nested inside one.
Generator Return Values
Like any function, a generator function can return a value rather than just falling off the end. That return
value shows up on the final call to next(), alongside done: true — but it is invisible to for…of and
the spread operator, both of which stop as soon as they see done: true and never look at that last value:
function* oneAndDone() {
yield 1;
return "finished";
}
[...oneAndDone()]; // => [1] -- the return value never appears here
let g = oneAndDone();
g.next(); // => { value: 1, done: false }
g.next(); // => { value: "finished", done: true }
g.next(); // => { value: undefined, done: true } -- calling again after done is a safe no-op
yield as a Two-Way Channel
yield is not only a way to send values out of a generator — it is an expression, and the value it
evaluates to is whatever gets passed into the next call to next(). That makes a generator and its caller
two cooperating, independently paused threads of execution that pass values back and forth in both directions:
function* smallNumbers() {
let a = yield 1; // pauses here; a gets whatever the *next* next() call passes in
let b = yield 2;
let c = yield 3;
return [a, b, c];
}
let g = smallNumbers();
g.next(); // => { value: 1, done: false } -- argument to this first call is discarded
g.next("x"); // => { value: 2, done: false } -- "x" becomes a
g.next("y"); // => { value: 3, done: false } -- "y" becomes b
g.next("z"); // => { value: ["x", "y", "z"], done: true } -- "z" becomes c
The asymmetry in that first call matters: it is what actually starts the generator running up to its first
yield, so whatever argument is passed to it has nowhere to go and is simply discarded.
return() and throw()
A running generator can also be steered from the outside without waiting for it to yield on its own, via two
methods every generator object exposes:
-
generator.return(value)makes the pausedyieldexpression behave as though areturn value;statement had appeared at that exact point — the generator’sdonebecomestrue,valuebecomes the returned value, and anyfinallyblock wrapping the paused code still runs. This is the generator counterpart to the iteratorreturn()method described above: use atry/finallyaround the generator body to guarantee cleanup runs however the generator stops, since a generator’sreturn()cannot be overridden the way a hand-written iterator’snext()/return()pair can. -
generator.throw(error)makes the pausedyieldexpression behave as thoughthrow error;had appeared there. If the generator body wraps that point in atry/catch, it can catch and handle the injected error instead of letting it propagate — a way to send an out-of-band signal (e.g. "reset", "cancel") into a running generator, not just a way to abort it.
function* countingUp() {
let n = 0;
try {
for (;;) {
try {
yield n++;
} catch (resetSignal) {
n = 0; // throw() was used as a "reset" signal, not a fatal error
}
}
} finally {
console.log("cleanup: counter stopped"); // runs on return() too
}
}
When a generator delegates to another iterable with yield*, calls to next(), return(), and throw() on
the outer generator all forward to the delegated-to iterable’s own next()/return()/throw() methods (if it
defines them), so cleanup and signaling propagate correctly through a chain of delegated generators, not just
the outermost one.
Generators and Async Iteration
Everything on this page describes synchronous iteration: next() always returns its result immediately.
Generators historically were also pressed into service to simulate asynchronous, sequential-looking code (pause
at a yield, resume later once an asynchronous operation completes) — but doing that by hand is intricate and
easy to get subtly wrong. The language now has a dedicated, purpose-built protocol for this instead: async
generators (async function*) and for await…of, which layer Promise-based waiting directly onto the same
{ value, done } shape described in this page. See Asynchronous
JavaScript for how await, for await…of, and async generators relate to the synchronous iterator protocol
covered here, and why they — not hand-rolled generator-based coroutines — are the current, idiomatic way to
express asynchronous sequences of values.