Skip to content

Iterator Helpers Bring Array Methods to Any Iterable

JavaScript's iterator helpers add map, filter, take, and more directly to any iterable, without the memory cost of building an intermediate array first.

· · 11 min read

Updated: August 23, 2026

A gray laptop computer showing code

Quick Take

I used to convert every custom iterator to an array just so I could call .filter() on it. Iterator helpers, now baseline across browsers and Node, end that workaround for good.

Run this yourself. The benchmark that produced every number in this article: iterator-helpers-benchmark/ in the Coding Dunia code-examples repo.

Generators are one of JavaScript's most underused features, partly because working with them used to mean writing a manual for...of loop for every transformation, or converting to an array and losing the laziness that made a generator worth using in the first place. Iterator helpers fix that by giving iterators the same fluent API arrays have had since forever.

Quick take: Iterator helpers add .map(), .filter(), .take(), .drop(), .flatMap(), .reduce(), and .toArray() directly to any iterator or generator, no array conversion required. They're lazy, so you can chain transformations over an infinite generator and only compute what you actually consume. Support for the TC39 proposal landed across Chrome, Firefox, Safari, and Node.js 22+ through 2025, and it's Baseline as of 2026.

What Problem Do Iterator Helpers Solve?

Say you have a generator that produces an infinite sequence, and you want the first five even numbers, squared:

function* naturalNumbers() {
  let n = 1;
  while (true) {yield n++;}
}

// Before iterator helpers: convert to array first, but you can't,
// the generator is infinite. So you write a manual loop instead.
function firstFiveEvenSquares() {
  const result = [];
  for (const n of naturalNumbers()) {
    if (n % 2 === 0) {
      result.push(n * n);
      if (result.length === 5) {break;}
    }
  }
  return result;
}

That works, but it's imperative, and every new transformation means editing the loop body. Have you ever added a third condition to a loop like this and had to re-read the whole function to make sure you got the order right?

What Does the Same Thing Look Like With Iterator Helpers?

function* naturalNumbers() {
  let n = 1;
  while (true) {yield n++;}
}

const firstFiveEvenSquares = naturalNumbers()
  .filter(n => n % 2 === 0)
  .map(n => n * n)
  .take(5)
  .toArray();

// [4, 16, 36, 64, 100]

The code reads in the order it executes: filter, then map, then take five, then materialize as an array. Nothing here converts the infinite generator to an array early, .take(5) stops pulling values the moment it has five, so .filter() and .map() only ever run against the first ten natural numbers, not an unbounded sequence. Iterator helpers are a set of methods, map, filter, take, drop, flatMap, reduce, and toArray among them, added directly to Iterator.prototype, per the TC39 proposal that shipped them, so every object implementing the iterator protocol inherits them for free.

Don't take my word for "only the first ten", instrument it and count. I added a counter to each callback and ran it on Node 22.11.0, the same build as the timings further down:

let mapCalls = 0, filterCalls = 0;

naturalNumbers()
  .filter(n => { filterCalls++; return n % 2 === 0; })
  .map(n => { mapCalls++; return n * n; })
  .take(5)
  .toArray();

console.log({ filterCalls, mapCalls });
// { filterCalls: 10, mapCalls: 5 }

.filter() ran 10 times (checking numbers 1 through 10 for evenness) and .map() ran exactly 5 times, once per even number found. An infinite generator, and the chain touched 10 values total to get its answer. That's not an approximation of laziness, it's the actual call count.

Factory pipework where every valve decides what passes through
Photo by Ricardo Gomez Angel on Unsplash

Why Does Laziness Matter?

Array.prototype.map() is eager: it runs the callback over every element and returns a full new array immediately. That's fine for a 50-item array. It's a problem for a generator reading lines from a multi-gigabyte log file, where you only want the first match.

function* readLines(source) {
  // imagine this yields one line at a time from a huge file
  for (const line of source) {yield line;}
}

const firstErrorLine = readLines(logSource)
  .filter(line => line.includes('ERROR'))
  .take(1)
  .toArray()[0];

With an array-based approach, you'd read the entire file into memory, filter it, then take the first result. With iterator helpers, reading stops the instant the first ERROR line is found. That's not a minor optimization, it's the difference between reading 10 lines and reading 10 million. A lazy iterator is one that only computes the next value when something actually asks for it, as opposed to an eager operation like Array.prototype.map() that runs its callback over every element up front regardless of how many results the caller ends up using. On a 10-million-line log file, that difference is not theoretical, it's the gap between an operation that returns in milliseconds and one that has to allocate and scan the entire file first.

What Does Laziness Cost When You Don't Stop Early?

The log-file example is the one every write-up reaches for, mine included, and it's the case where laziness looks free. It isn't free. Iterator helpers pull one value at a time through the iterator protocol, and that protocol has a per-value price that Array.prototype.map() doesn't pay. Nobody seems to publish the number, so I measured it.

Setup: one million plain objects ({ id, score, name }), Node 22.11.0 on V8 12.4, Apple M1, run on 2026-08-23. Each figure is a timed batch divided by its iteration count, best of three batches, after a warm-up pass. Heap growth came from process.memoryUsage().heapUsed sampled every millisecond under --expose-gc. Three workloads, same data:

// A. first 10 matches out of a million
data.filter(r => r.score > 900).map(r => r.name).slice(0, 10);
data.values().filter(r => r.score > 900).map(r => r.name).take(10).toArray();

// B. every match (96,288 of them)
data.filter(r => r.score > 900).map(r => r.name);
data.values().filter(r => r.score > 900).map(r => r.name).toArray();

// C. sum a mapped field across all one million
data.map(r => r.score).reduce((a, b) => a + b, 0);
data.values().map(r => r.score).reduce((a, b) => a + b, 0);
WorkloadArray methodsIterator helpers
A. First 10 matches8.10 ms0.012 ms
B. All 96,288 matches8.00 ms15.8 ms
C. Sum across all 1,000,00013.3 ms29.5 ms
A. heap growth+3.4 MB+0.0 MB
One million objects, Node 22.11.0 on Apple M1 (ms, lower is better) array methods iterator helpers A. first 10 8.10 0.012 B. all matches 8.00 15.8 C. sum of 1M 13.3 29.5

Workload A is the headline anyone would expect, and it's bigger than I expected: about 690 times faster, plus 3.4 MB of intermediate array that never gets allocated. .filter() on the array builds a 96,288-entry result, .map() builds a second one, and .slice(0, 10) throws away 96,278 of them. The lazy chain touches roughly a hundred objects and stops.

Workloads B and C are the part that doesn't get written down. Consume the whole chain and iterator helpers are consistently about twice as slow: 15.8 ms against 8.00 ms, 29.5 ms against 13.3 ms. That ratio held across every re-run. Array methods run a tight loop over a contiguous backing store with a monomorphic callback, which V8 optimizes hard. Iterator helpers call .next() per value, per stage, allocating a result object each time, and V8 has much less room to work with.

One more number worth having, because it reframes the whole comparison. The same "first 10 matches" job written as an ordinary for..of loop with a break ran in 0.0007 ms, about 16 times faster than the helper chain. Helpers aren't the floor, they're a readable way to get within an order of magnitude of it. If a hot path is genuinely hot, a hand-written loop still wins, and it always did.

So the rule I'd actually apply: reach for iterator helpers when the source is unbounded, expensive per item, or you're going to stop early, which is exactly the log-file case. Reach for array methods when you already have an array in hand and you're going to walk all of it. And treat "iterator helpers are the fast new way to do map" as the wrong mental model, because on the workload most people actually run, they aren't.

Usual caveats: single machine, one engine, Apple silicon. The 2x penalty and the 690x saving are ratios I'd expect to travel; the milliseconds are not portable, and V8 tunes this code path release to release.

Parallel metal pipes running the length of a plant ceiling
Photo by Samuel Sianipar on Unsplash

Which Methods Are Available?

MethodWhat it doesEager or lazy
.map(fn)Transform each valueLazy
.filter(fn)Keep values matching a predicateLazy
.take(n)Stop after n valuesLazy
.drop(n)Skip the first n valuesLazy
.flatMap(fn)Map then flatten one levelLazy
.reduce(fn, init)Fold into a single valueEager, terminal
.toArray()Materialize as an arrayEager, terminal
.forEach(fn)Run a side effect per valueEager, terminal
.some(fn) / .every(fn)Boolean testsEager, terminal (short-circuits)

The pattern to remember: everything that returns another iterator is lazy and chainable. Everything that returns a concrete value, a number, a boolean, an array, is a terminal operation that actually pulls values through the whole chain. Per MDN, nine methods make up the current spec: map, filter, take, drop, flatMap, reduce, toArray, forEach, and the boolean pair some/every, and five of them are lazy while four are terminal, a ratio worth remembering since it's roughly even between the two categories. .reduce() deserves a specific callout: unlike Array's version it works directly against any iterator, so a running total or a grouped object can be built from a generator without a .toArray() step in between, one fewer allocation for a pattern that shows up in almost every data-processing script.

How Do You Use Iterator Helpers on Built-in Iterables?

They aren't limited to generator functions. Map.prototype.keys(), Map.prototype.values(), and Set.prototype.values() all return iterators, and they get the same helper methods:

const inventory = new Map([
  ['widget', 42],
  ['gadget', 0],
  ['gizmo', 17],
]);

const inStockNames = inventory
  .entries()
  .filter(([, count]) => count > 0)
  .map(([name]) => name)
  .toArray();

// ['widget', 'gizmo']

No Array.from(inventory.entries()) needed as a first step. The .entries() iterator itself now supports the full chain.

How Do You Write Your Own Iterable That Benefits?

Any class implementing Symbol.iterator gets these methods automatically, since the return value of Symbol.iterator is expected to be an iterator, and Iterator.prototype now sits in that chain. Per the TC39 proposal that introduced this, the change was made at the prototype level specifically so it would apply retroactively to every existing iterable class in every codebase already written, without requiring a single line of code to change, three or four years' worth of custom iterator classes across the ecosystem gained these methods the moment engines shipped support:

class Range implements Iterable<number> {
  constructor(private start: number, private end: number) {}

  *[Symbol.iterator]() {
    for (let i = this.start; i < this.end; i++) {yield i;}
  }
}

const range = new Range(0, 1000000);
const firstTenSquares = range[Symbol.iterator]()
  .map(n => n * n)
  .take(10)
  .toArray();

A million-element Range costs nothing extra here beyond computing ten squares, because nothing before .take(10) runs eagerly.

The habit worth breaking is reaching for Array.from() the moment you see a generator, before you know whether you actually need the whole thing materialized. Half the time you don't, and the iterator itself already has the method you were about to write a loop for.

Frequently Asked Questions

What are JavaScript iterator helpers?
Iterator helpers are a set of methods, map, filter, take, drop, flatMap, reduce, toArray, forEach, and a few more, added directly to the Iterator prototype. Before this, only arrays had these methods. Any object implementing the iterator protocol, a generator function, a Map's keys(), a custom class with Symbol.iterator, now gets them for free without you converting it to an array first.
Do I still need Array.from() with iterator helpers?
Only when you actually need an array at the end, for example to pass to a function that expects one, or to use array-only methods like sort(). If you're just chaining transformations and consuming the result with a for...of loop, you don't need Array.from() at all, iterator helpers let you skip the intermediate array entirely.
Are iterator helpers lazy?
Yes, and that's the main reason they exist. .map() on an iterator doesn't run the mapping function over every item immediately, it returns a new iterator that applies the mapping only as each value is pulled. Combined with .take(n), you can process an infinite generator and only ever compute the first n values, something Array.prototype.map() can't do because arrays are eager and finite by definition.