JavaScript Iterators and Generators
Understand the iterable and iterator protocols, build custom iterables, use generators and delegation, reason about iterator closing, and compose lazy modern iterator pipelines.
Intermediate
Iterators are the protocol behind JavaScript iteration. An iterable knows how to produce an iterator; an iterator produces results one step at a time. Generators are a compact way to create iterators, but the protocol is larger than generator syntax. This lesson builds the protocol first, then uses generators, delegation, closing, and modern iterator helpers.
The mental model: iterable → iterator → result
When a value is consumed by for...of, JavaScript asks it for an iterator through Symbol.iterator. The iterator then exposes next(), and each call returns an IteratorResult shaped like { value, done }. The consumer decides when to ask for another result.
Output
Ready to run.
Quick check
Which object is responsible for producing the next IteratorResult?
const iterator = [1, 2][Symbol.iterator]();
Iterable versus iterator
The roles can be implemented by different objects. Arrays are iterable, while their iterator objects are stateful consumers. A custom object can also return itself from Symbol.iterator when it already is its own iterator.
Output
Ready to run.
Quick check
What must Symbol.iterator return?
const iterable = { [Symbol.iterator]() { return ???; } };
IteratorResult is a protocol record, not a special class
The protocol cares about the observable properties value and done. A custom iterator does not need to inherit from a built-in Iterator class. Once done is true, consumers should treat the iterator as exhausted.
Output
Ready to run.
Quick check
After an iterator reports done: true, what should later next() calls report?
const iterator = { next() { return { value: 1, done: true }; } };
for...of consumes the iterator for you
for...of does not enumerate arbitrary object properties. It obtains an iterator and repeatedly asks for values until the iterator reports completion. This is why arrays, strings, Sets, Maps, and custom iterables can all participate without sharing the same storage model.
Output
Ready to run.
Quick check
Why does for...of work with a Set?
for (const value of new Set([1, 2])) { /* ... */ }
One iterator has state; repeated iteration usually creates new state
An iterator is a cursor, not a reusable collection. Calling an iterable’s Symbol.iterator method normally creates a fresh iterator. Reusing the same iterator continues from its current position.
Output
Ready to run.
Quick check
What happens when you call Symbol.iterator on the same array twice?
const a = [1, 2]; const first = a[Symbol.iterator](); const second = a[Symbol.iterator]();
Generators turn suspended execution into an iterator
A generator function uses function* and yield. Calling it does not run the body to completion. It returns a generator object that is both iterable and an iterator, and each next() resumes execution until the next yield or completion.
Output
Ready to run.
Quick check
What does calling a generator function immediately return?
function* values() { yield 1; } const iterator = values();
yield receives a value from the next() call
A yield expression pauses the generator and can later evaluate to the value supplied to the next(value) call that resumes it. The first next() call starts the generator and its argument is not delivered to a preceding yield because there is none yet.
Output
Ready to run.
Quick check
Which call supplies the value received by const answer = yield ...?
function* g() { const value = yield 1; } const it = g(); it.next(); it.next(42);
yield* delegates to another iterable
yield* forwards iteration to another iterable. The outer generator can surround that delegated sequence with its own values, while the delegated iterator handles its own next/return/throw protocol interactions.
Output
Ready to run.
Quick check
What does yield* do in this example?
function* g() { yield* [1, 2]; }
Early exit can close an iterator
Some iterators expose return(). When a for...of loop exits early, for example through break, the loop can call return() to give the iterator an opportunity to close resources or finish its protocol state. Generators provide this behavior automatically.
Output
Ready to run.
Quick check
What is the important reason to implement return() on a custom iterator?
const iterator = { next() { return { done: false, value: 1 }; }, return() { /* cleanup */ } };
Iterator helpers are lazy
Iterator helpers extend Iterator.prototype with lazy pipeline methods: map, filter, flatMap, take, and drop; terminal methods such as reduce, some, every, find, forEach, and toArray consume the iterator. The key distinction is that pipeline helpers usually produce another iterator, while terminal helpers consume values until their result is determined or the source ends. This lesson owns these Iterator helpers; Array.prototype methods remain the Array lesson.
Output
Ready to run.
Quick check
What makes map(), filter(), and take() different from terminal helpers such as reduce() and toArray()?
const mapped = [1, 2, 3].values().map(x => x * 2);
drop skips values lazily
Iterator.prototype.drop(limit) consumes and discards the requested number of values before yielding the rest. It is lazy: creating the helper does not skip source values until the helper is consumed. The limit is converted to a number and must not be negative or NaN.
Output
Ready to run.
Quick check
What does drop(2) do when its resulting iterator is consumed?
const result = Iterator.from([1, 2, 3]).drop(1);
flatMap flattens mapped iterators
Iterator.prototype.flatMap(mapper) maps each source value to an iterable or iterator and then yields the values from those inner sequences in order. It is still lazy: an inner sequence is advanced only as output is requested.
Output
Ready to run.
Quick check
What is the defining extra step performed by flatMap()?
const expanded = Iterator.from([1, 2]).flatMap(n => [n, n * 10]);
reduce consumes into one accumulator
Iterator.prototype.reduce(reducer[, initialValue]) consumes the source and carries an accumulator forward. With an initial value, the first callback receives that value and the first source value. Without one, the first source value becomes the accumulator; reducing an empty iterator without an initial value throws a TypeError.
Output
Ready to run.
Quick check
What happens when reduce() receives an empty iterator with no initial value?
Iterator.from([]).reduce((a, b) => a + b);
some and every can stop early
some(predicate) returns true as soon as a predicate succeeds; otherwise it consumes the source and returns false. every(predicate) returns false as soon as a predicate fails; otherwise it consumes the source and returns true. Both are terminal operations and can therefore stop before the source is exhausted.
Output
Ready to run.
Quick check
Why can some() and every() avoid consuming the entire iterator?
Iterator.from([1, 3, 4]).some(x => x % 2 === 0);
find returns the first matching value
Iterator.prototype.find(predicate) consumes values until the predicate is truthy. It returns the first matching value, or undefined if the source ends without a match. Because it is terminal and short-circuiting, later source values may never be requested.
Output
Ready to run.
Quick check
What does find() return when no value satisfies its predicate?
Iterator.from([1, 2, 3]).find(x => x > 10);
forEach consumes for side effects
Iterator.prototype.forEach(procedure) consumes the iterator and calls the procedure for each value. Unlike map(), it does not create a new iterator. The method returns undefined after normal completion.
Output
Ready to run.
Quick check
What does iterator forEach() return after normal completion?
const result = Iterator.from([1, 2, 3]).forEach(console.log);
Iterator.from adapts an iterable or iterator
Iterator.from creates an iterator wrapper around an iterable or iterator. This lets code use the modern Iterator prototype methods without first materializing the entire sequence into an array.
Output
Ready to run.
Quick check
What is a useful reason to use Iterator.from?
const iter = Iterator.from([1, 2, 3]);
Design a custom iterable around a real state machine
A good custom iterable separates collection state from cursor state. Keep the collection reusable, return a fresh iterator when independent traversals are expected, and make the iterator’s completion behavior explicit. This pattern is more important than memorizing generator syntax.
Output
Ready to run.
Quick check
Why should a reusable collection usually return a new iterator?
const collection = { [Symbol.iterator]() { return /* fresh iterator */; } };
Zip multiple iterables with Iterator.zip
Iterator.zip combines several iterables position-by-position. The default mode is "shortest": iteration stops when the first input finishes. The method also supports "longest" with padding and "strict", which throws when the inputs have different lengths. These are iterator operations, not array-only utilities, so the inputs are consumed through the iterable protocol.
Output
Ready to run.
Quick check
What does Iterator.zip use by default when the input iterables have different lengths?
const result = Iterator.zip([[1, 2, 3], ["a", "b"]]).toArray();
Strict zipping detects length mismatches
The "strict" mode is useful when equal-length inputs are part of the data contract. If one input ends before another, the zipped iterator throws a TypeError instead of silently truncating or padding the result. The important distinction is that strictness is a mode of the zip operation; it does not change the underlying iterator protocol.
Output
Ready to run.
Quick check
What should strict mode do when the input iterables have different lengths?
const result = Iterator.zip([[1, 2, 3], ["a", "b"]], { mode: "strict" });
Zip keyed iterables into objects
Iterator.zipKeyed is the keyed counterpart to Iterator.zip. Instead of producing arrays for each row, it takes an object whose own keys identify the input iterables and produces an object with those same keys for each row. It uses the same shortest, longest, and strict modes.
Output
Ready to run.
Quick check
What shape does Iterator.zipKeyed({ name: names, score: scores }) produce for each iteration?
const rows = Iterator.zipKeyed({ name: ["Ada"], score: [10] }).toArray();
Zipping also has iterator-closing semantics
Iterator.zip is responsible for coordinating the iterators it opens. When the zip operation finishes early, fails, or is closed, the specification closes still-open underlying iterators through their iterator-closing protocol. This is related to iterator cleanup, but it is not the same topic as the explicit resource-management APIs taught in the Explicit Resource Management lesson. Here, the ownership boundary is the iterator protocol: return() is the cleanup hook that an iterator may expose.
Output
Ready to run.
Quick check
Which iterator-level hook is used when Iterator.zip needs to close an underlying iterator?
const iterator = { next() { return { done: false, value: 1 }; }, return() { return { done: true }; } };
Iterator.concat() — sequence iterator sources
Iterator.concat creates one iterator that consumes the supplied iterator or iterable sources in sequence. It is a static Iterator operation, distinct from the prototype helpers that transform or consume one iterator.
Output
Ready to run.
Quick check
What does Iterator.concat() produce?
const result = [...Iterator.concat([1].values(), [2].values())];
document.querySelector("#output").textContent = result.join(", ");
Authoring rules
Choose an iterable when callers should be able to start traversal through Symbol.iterator. Choose an iterator when the caller needs a cursor with next(). Prefer generators when a sequential state machine is clearer than hand-written next() logic. Use lazy iterator helpers for pipelines, and use terminal helpers when you intentionally want to consume values into a result or side effect. Remember that toArray() is the explicit point where the pipeline becomes an array; reduce(), some(), every(), find(), and forEach() also consume their source.
Practice
Build a reusable iterable that yields only the even numbers in a numeric range. Then build a generator that delegates to that iterable with yield*. Finally, add an Iterator helper pipeline that takes the first two yielded values and converts them to an array. Explain which part is the collection, which part is the cursor, and where laziness ends.
Specification references
Normative source: ECMA-262, current living specification, especially §27.1 Iterator Objects and the Iterator.prototype methods drop, every, filter, find, flatMap, forEach, map, reduce, some, take, and toArray. The curriculum treats iterator protocols as ECMAScript language semantics; host scheduling and browser-specific asynchronous behavior are covered later in the browser/runtime track.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.