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.
TL;DR: 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 landed across Chrome, Firefox, Safari, and Node.js 22+ through 2025, and it's Baseline as of 2026.
The Problem 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?
The Same Thing 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]
This 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.
Why Laziness Matters
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.
Which Methods Are Available
| Method | What it does | Eager or lazy |
|---|---|---|
.map(fn) | Transform each value | Lazy |
.filter(fn) | Keep values matching a predicate | Lazy |
.take(n) | Stop after n values | Lazy |
.drop(n) | Skip the first n values | Lazy |
.flatMap(fn) | Map then flatten one level | Lazy |
.reduce(fn, init) | Fold into a single value | Eager, terminal |
.toArray() | Materialize as an array | Eager, terminal |
.forEach(fn) | Run a side effect per value | Eager, terminal |
.some(fn) / .every(fn) | Boolean tests | Eager, 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.
Using 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.
Writing 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:
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.
Conclusion
Iterator helpers close a real gap: arrays had a fluent, chainable API for two decades, and every other iterable had to be converted to an array first to get the same ergonomics, at the cost of losing laziness and sometimes blowing up memory on large or infinite sequences. If you're writing a generator function in 2026 and reaching for Array.from() out of habit, check whether .filter(), .map(), or .take() on the iterator itself gets you there without the conversion.