Skip to content
All posts
#Performance#Architecture

Iterator Helpers: Pipelines That Never Materialize an Array

6 min read

Iterator helpers map, filter and take over a Map, a generator or an infinite sequence without ever spreading it into an array — plus the one footgun that shares its source.

Iterator Helpers: Pipelines That Never Materialize an Array

Most frontend code that walks a collection reaches for an array method. That is a good default until the collection is a Map, a generator, or a stream you never wanted to hold in memory — and then the first move, spreading it into an array, discards the one property that made the source worth using.[5]

The Intermediate Array You Never Wanted

The classic pattern converts an iterator to an array before doing anything with it.[1] MDN uses a Map of names to deposits as the example, and notes that spreading Map.prototype.values() into an array and then calling Array.prototype.reduce() "both creates an intermediate array and iterates the array twice."[1]

The iterator version does neither. Calling reduce() on the iterator itself "may be more efficient, especially memory-wise, because it only iterates the iterator once, without memorizing any intermediate values."[1]

For a four-entry Map that difference is noise. For a generator reading a large NDJSON file it is the difference between streaming and holding the whole payload in memory.

Two Protocols, One Class

The confusion to clear up first is that "iterable" and "iterator" are not the same contract. MDN documents them as separate protocols: an object is iterable when it implements a [Symbol.iterator]() method, and it is an iterator when it implements next().[4] Neither implies the other.

The Iterator class is the abstract base that ties them together for built-ins. web-features describes it as "an abstract base for objects that implement the iterator protocol," supplying the methods common to built-in iterators.[9] Every built-in iterator — from Array.prototype.values(), from Map.prototype.entries(), from a generator — inherits from Iterator.prototype, which provides a [Symbol.iterator]() that returns the iterator itself. That single method is why a bare iterator works directly in for...of.[1]

Inheritance alone does not make something an iterator, though. MDN is explicit that the two "do not entail each other — objects that inherit from Iterator do not automatically become iterators, because the Iterator class does not define a next() method."[1] You still have to write next() yourself.

Laziness Is the Whole Point

filter(), map(), flatMap(), take(), and drop() return iterator helper objects rather than arrays.[1] A helper is itself an Iterator instance, which is what makes the chain composable, and it is why none of those five steps does any work until you pull a value.

The practical rule MDN states for toArray() follows from that: it is "a good idea to call toArray() as a last step of your processing," because fibonacci().take(10).toArray().filter(...) is less efficient — iterator helpers are lazy and avoid creating a temporary array.[3]

Laziness is also the only way to work with a source that never ends. MDN notes that iterator helper methods "are necessary to work with infinite iterators," and gives a Fibonacci generator as the case: you cannot convert it to an array, but find() on the iterator walks only as far as it must.[1]

The Footgun: Helpers Share the Source

The most common production bug with this API is treating a helper as a snapshot. It is not one. MDN states it plainly: "The iterator helper shares the same data source as the underlying iterator, so iterating the iterator helper causes the underlying iterator to be iterated as well. There is no way to 'fork' an iterator to allow it to be iterated multiple times."[1]

Its own example is the trap:

js
const it = [1, 2, 3].values();
const it2 = it.drop(0); // Essentially a copy
console.log(it.next().value);  // 1
console.log(it2.next().value); // 2
console.log(it.next().value);  // 3

it2 is not a copy. Both cursors advance the same source, so consuming one consumes the other.[1] If you need to iterate twice, materialize once with toArray() and stop pretending the iterator is reusable.

The Protocol Passes Through

Iterator helpers are deliberately transparent to the calls they wrap.[6] The TC39 design notes record that "all added methods attempt to pass the values and calls they receive to whatever iterator they are wrapping" — it.map(fn).next(5) calls it.next(5), and it.map(fn).return() propagates upward to it.return().[6]

That second half matters more than it looks. return() is the hook a generator's finally block runs on, and it is what releases resources held by an async iterator.[unverified] Because the helper forwards it, breaking out of a for...of over a mapped iterator still runs your cleanup.

Iterator.from() Normalizes Foreign Iterators

Libraries hand back objects that satisfy the iterator protocol but do not inherit from Iterator, so they have next() and none of the helpers.[2] Iterator.from() is the adapter: it "creates a new Iterator object from an iterator or iterable object," and MDN frames its purpose as converting "custom iterators, probably exported by libraries, to proper iterators."[2]

It is also idempotent for already-proper iterators — if the object already has Iterator.prototype in its chain, Iterator.from() returns it unchanged rather than wrapping it.[2]

What Shipped, and What Is Still in Flight

The sync helpers reached Stage 4 and were merged into ECMA-262.[5] Per browser-compat-data, the core set — Iterator, from, map, filter, take, drop, flatMap, reduce, toArray — shipped in Chrome 122, Firefox 131, Safari 18.4, and Node.js 22.0.0.[8] MDN dates Baseline newly available to March 2025.[2]

The newer additions are still landing unevenly. Iterator.concat() is in Chrome 146, Firefox 147, and Safari 26.4, while Iterator.zip() and zipKeyed() are in Chrome 153 and Firefox 148 with Safari still at preview.[8] join() and includes() reached Chrome 153 and 154 respectively, and chunks() and windows() remain experimental — Firefox 154 only — with their web-features entry still filed under the draft directory.[8][10]

Async is further out. The async helpers were split into their own proposal to resolve concurrency questions, and it sits at Stage 2 with a caveat: the API is unlikely to change, but "the underlying specification mechanism will likely be radically revised."[7] The open question is whether map() over an async iterator may start two fetch calls at once — the original design, built as async generators, buffered .next() calls instead.[7]

What to Take From It

Reach for toArray() when you are done, not when you start.[3] Treat a helper chain as a single-use cursor over one source, not a value you can branch. And when a library hands you something iterator-shaped that lacks the helpers, Iterator.from() is the one call that fixes it without a copy.[2]

Sources

[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator — MDN — Iterator (JavaScript)

[2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/from — MDN — Iterator.from()

[3] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/toArray — MDN — Iterator.prototype.toArray()

[4] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols — MDN — Iteration protocols

[5] https://raw.githubusercontent.com/tc39/proposal-iterator-helpers/main/README.md — TC39 — Iterator Helpers proposal (Stage 4)

[6] https://raw.githubusercontent.com/tc39/proposal-iterator-helpers/main/DETAILS.md — TC39 — Iterator Helpers: DETAILS.md (semantics decisions)

[7] https://raw.githubusercontent.com/tc39/proposal-async-iterator-helpers/main/README.md — TC39 — Async Iterator Helpers proposal (Stage 2)

[8] https://raw.githubusercontent.com/mdn/browser-compat-data/main/javascript/builtins/Iterator.json — mdn/browser-compat-data — javascript/builtins/Iterator.json

[9] https://raw.githubusercontent.com/web-platform-dx/web-features/main/features/iterator-methods.yml — web-features — iterator-methods.yml

[10] https://raw.githubusercontent.com/web-platform-dx/web-features/main/features/draft/spec/tc39-iterator-chunking.yml — web-features — tc39-iterator-chunking.yml (draft)