Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

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.

Levels: Intermediate ~40 min read

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Was this page helpful?

Mark this learning resource complete to track your learning progress.