JavaScript (ES2026) Module 10 – Iterators and generators
Iterator helpers: lazy map, filter, take and more
JavaScript iterator helpers: lazy map, filter, take, drop and flatMap, reduce and toArray, Iterator.from, early exit, and when to use them over array methods.
What you will learn
- Build lazy chains of operations on any iterator with the iterator helpers of ES2025
- Use Iterator.from to give helpers to strings, plain iterators and other iterables
- Predict how many values a chain of helpers pulls and when it closes its source
- Choose between array methods and iterator helpers for a job
Before you start
What you’ll learn · Before you start
What you will learn
- Build lazy chains of operations on any iterator with the iterator helpers of ES2025
- Use Iterator.from to give helpers to strings, plain iterators and other iterables
- Predict how many values a chain of helpers pulls and when it closes its source
- Choose between array methods and iterator helpers for a job
Before you start
On this page
Iterator helpers are methods that every built-in iterator inherits from Iterator.prototype: map, filter, take,
drop and flatMap, which return a new iterator, and reduce, toArray, forEach, some, every and find,
which consume one. They read like array methods, but they are lazy. A chain such as
naturals().filter(isEven).map(square).take(3) does nothing until something asks it for values, and then it pulls
one value at a time through every step, so it works on endless sequences and stops as soon as it has enough. Arrays
are not iterators, so array.values() or Iterator.from(array) comes first. The helpers arrived in ES2025 and run in
Chrome 122, Firefox 131, Safari 18.4 and Node.js 22 or later, and in the engine of this page’s Run buttons. This
lesson counts exactly how many values a chain pulls, shows when it closes its source and lists the places where a
helper behaves differently from the array method with the same name.
Lazy chains on any iterator
Both halves of this program compute the squares of the first three even numbers. The first asks an endless generator through helpers; the second asks a million-element array through array methods. Each callback counts its calls:
// The squares of the first three even numbers, asked of an endless iterator and of a million-element array.
const lazy = { produced: 0, filter: 0, map: 0 };
function* naturals() {
for (let n = 1; ; n++) {
lazy.produced += 1;
yield n;
}
}
const fromIterator = naturals()
.filter((n) => {
lazy.filter += 1;
return n % 2 === 0;
})
.map((n) => {
lazy.map += 1;
return n * n;
})
.take(3)
.toArray();
console.log(fromIterator, lazy);
// Arrays need a finite source, and each method runs over every element before the next method starts.
const eager = { filter: 0, map: 0 };
const numbers = Array.from({ length: 1_000_000 }, (_, i) => i + 1);
const fromArray = numbers
.filter((n) => {
eager.filter += 1;
return n % 2 === 0;
})
.map((n) => {
eager.map += 1;
return n * n;
})
.slice(0, 3);
console.log(fromArray, eager); Output
[ 4, 16, 36 ] { produced: 6, filter: 6, map: 3 }
[ 4, 16, 36 ] { filter: 1000000, map: 500000 }
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node lazy-pipeline.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
How a chain of helpers pulls one value at a time
Text description of the diagram
The diagram shows the chain `naturals().filter(n => n % 2 === 0).map(n => n * n).take(3).toArray()` from lazy-pipeline.mjs as five boxes, the consumer at the top and the source at the bottom.
Solid arrows point down: each stage asks the stage below it for a value with `next()`. `toArray()` asks `take(3)`, which asks `map`, which asks `filter`, which asks `naturals()`. `filter` keeps asking until a value passes its test: it receives 1, rejects it, asks again and receives 2.
Dashed arrows point up and carry one value: 2 goes from `filter` to `map`, which turns it into 4, and 4 goes up through `take` to `toArray()`. Only then does the chain ask for the next value. After three values, `take` reports done and closes the stages below it, so `naturals()` never makes the seventh number.
The helper chain asked the generator for 6 numbers, ran filter 6 times and map 3 times, and stopped. toArray()
started the work: it asked take(3) for a value, take asked map, map asked filter, and filter kept asking
the generator until a number passed its test. After the third square, take answered done and closed everything
below it, so the endless generator never produced a seventh number.
The array chain produced the same answer after calling filter 1,000,000 times and map 500,000 times, and only
because the array was finite. Array methods are eager: filter builds a complete new array before map sees
its first element, and slice throws away almost all the work. Given an endless source, the array version could not
start at all, because Array.from would never finish.
One value at a time through the chain
The order of the callbacks shows the difference directly:
// The order in which the callbacks run: one value at a time through the chain, or each method over everything.
const viaHelpers = [];
[1, 2, 3, 4]
.values()
.filter((n) => {
viaHelpers.push(`filter ${n}`);
return n % 2 === 0;
})
.map((n) => {
viaHelpers.push(`map ${n}`);
return n * 10;
})
.forEach((n) => viaHelpers.push(`got ${n}`));
console.log('helpers:', viaHelpers.join(' → '));
const viaArrays = [];
[1, 2, 3, 4]
.filter((n) => {
viaArrays.push(`filter ${n}`);
return n % 2 === 0;
})
.map((n) => {
viaArrays.push(`map ${n}`);
return n * 10;
})
.forEach((n) => viaArrays.push(`got ${n}`));
console.log('arrays: ', viaArrays.join(' → ')); Output
helpers: filter 1 → filter 2 → map 2 → got 20 → filter 3 → filter 4 → map 4 → got 40 arrays: filter 1 → filter 2 → filter 3 → filter 4 → map 2 → map 4 → got 20 → got 40
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node order.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
With helpers, each value travels through the whole chain before the next one is read: 2 passes filter, is mapped
to 20 and reaches forEach before filter sees 3. With arrays, every method finishes its pass over all elements
before the next method begins. Results are the same for pure callbacks, but laziness changes when side effects
happen, how much memory the intermediate steps need (helpers build no intermediate arrays) and how early a chain can
stop.
The helpers, one by one
// Every helper once, on the runs scored in six overs.
const overs = () => [12, 0, 47, 3, 88, 4].values(); // a new iterator each time: helpers use up their source
// Lazy helpers return a new iterator.
console.log(overs().map((runs, i) => `over ${i + 1}: ${runs}`).toArray()); // the second argument counts from 0
console.log(overs().filter((runs) => runs >= 10).toArray());
console.log(overs().take(2).toArray(), overs().drop(4).toArray());
console.log(overs().flatMap((runs) => (runs > 40 ? [runs, 'big over'] : [runs])).toArray());
// Eager helpers consume the iterator and return a result.
console.log(overs().reduce((total, runs) => total + runs, 0));
console.log(overs().some((runs) => runs === 0), overs().every((runs) => runs >= 0), overs().find((runs) => runs > 40));
overs()
.take(2)
.forEach((runs) => console.log('forEach:', runs)); Output
[ 'over 1: 12', 'over 2: 0', 'over 3: 47', 'over 4: 3', 'over 5: 88', 'over 6: 4' ] [ 12, 47, 88 ] [ 12, 0 ] [ 88, 4 ] [ 12, 0, 47, 'big over', 3, 88, 'big over', 4 ] 154 true true 47 forEach: 12 forEach: 0
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node helpers-tour.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
| Helper | Returns | Ends early? |
|---|---|---|
map(fn), filter(fn), flatMap(fn) |
a new lazy iterator | no; it follows its consumer |
take(n), drop(n) |
a new lazy iterator | take stops after n values and closes its source |
reduce(fn, start), toArray(), forEach(fn) |
a value, an array, nothing | no; they read to the end |
some(fn), every(fn), find(fn) |
a boolean or the value found | yes, at the first answer |
Like their array namesakes, the callbacks receive a counter after the value: how many values came before, starting
at 0, as the over 1 to over 6 labels show. reduce passes the running result first, then the value and the
counter. Unlike array callbacks, none of them receives the collection itself, since an iterator has no array to pass.
Notice the factory overs(): each line calls it for a new iterator, because a helper uses up the iterator it reads
from.
Getting an iterator from any collection
Helpers are methods of iterators, so the first step is always to get one:
// Helpers live on iterators, not on arrays: how to get an iterator from each kind of collection.
const scores = [52, 17, 88];
console.log(typeof scores.take, typeof scores.values().take); // arrays have no take(); their iterators do
const runs = new Map([
['Asha', 52],
['Ravi', 17],
]);
console.log(runs.keys().map((name) => name.toUpperCase()).toArray());
console.log(runs.entries().filter(([, total]) => total >= 50).toArray());
console.log(new Set(['js', 'css']).values().map((tag) => `#${tag}`).toArray());
console.log(Iterator.from('a😀b').map((ch) => ch.codePointAt(0).toString(16)).toArray()); // a string, by code point
// A hand-made iterator does not inherit the helpers; Iterator.from wraps it.
const handMade = {
i: 0,
next() {
return this.i < 3 ? { value: this.i++, done: false } : { value: undefined, done: true };
},
};
console.log(typeof handMade.map);
console.log(Iterator.from(handMade).map((n) => n * 2).toArray());
// A class that extends Iterator gets them by inheritance.
class Countdown extends Iterator {
constructor(from) {
super();
this.n = from;
}
next() {
return this.n > 0 ? { value: this.n--, done: false } : { value: undefined, done: true };
}
}
console.log(new Countdown(3).map((n) => `T-${n}`).toArray());
const gen = (function* () {
yield 1;
})();
console.log(Iterator.from(gen) === gen); // already an Iterator: returned as it is Output
undefined function [ 'ASHA', 'RAVI' ] [ [ 'Asha', 52 ] ] [ '#js', '#css' ] [ '61', '1f600', '62' ] undefined [ 0, 2, 4 ] [ 'T-3', 'T-2', 'T-1' ] true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node get-iterators.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
- An array has no
take; its iterator does.array.values(),array.keys()andarray.entries()give one, and so do the same methods of aMapor aSet. - A generator object already inherits from
Iterator.prototype, so the helpers work on it directly, as innaturals().filter(…). Iterator.from(value)turns anything iterable into a helper-ready iterator: a string, which it walks by code point, an array or any object with a[Symbol.iterator]()method. It also accepts an object that only hasnext(), like the hand-made iterator that has nomapof its own, and wraps it. When the iterator already inherits fromIterator.prototype, as a generator does,Iterator.fromreturns it unchanged.- A class that
extends Iteratorand definesnext()inherits every helper, asCountdowndoes.Iteratoritself cannot be called withnew; it exists to be extended.
Stopping early closes the source
find, some, every and take stop reading once they have their answer, and they tell the iterator below them by
calling its return() method. The call passes down the whole chain, so a generator at the bottom runs its finally
block:
// Stopping early closes the source: find() and take() call return() back through the whole chain.
function* logLines() {
let read = 0;
try {
for (let n = 1; n <= 100_000; n++) {
read += 1;
yield n === 4_217 ? `ERROR payment ${n} declined` : `INFO payment ${n} ok`;
}
} finally {
console.log(` source closed after ${read} lines`);
}
}
const firstError = logLines().find((line) => line.startsWith('ERROR'));
console.log(firstError);
const firstIds = logLines()
.filter((line) => line.startsWith('INFO'))
.map((line) => Number(line.split(' ')[2]))
.take(3)
.toArray();
console.log(firstIds);
console.log(logLines().some((line) => line.includes('payment 2 '))); Output
source closed after 4217 lines ERROR payment 4217 declined source closed after 3 lines [ 1, 2, 3 ] source closed after 2 lines true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node log-scan.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Version note
In the engine of the Run button, QuickJS (quickjs-emscripten 0.32.0), this file prints the same. That engine has a
defect worth knowing when you edit the examples: when a helper such as map or filter is stopped early, by a
take, find, some or every after it or by a break in the loop that reads it, it throws
TypeError: not a function there if it reads from the iterator of an array, a Set, a Map or a string. Node.js
and the browsers close such chains correctly, and chains that read from a generator are not affected, as in this
file.
find read 4,217 lines, found the error on the last of them and closed the generator; the remaining 95,783 lines
were never produced. The filter, map and take(3) chain read exactly 3 lines, and some stopped at line 2. For a
generator that reads a real file or a network stream, this is what releases the file or the connection, which an
array pipeline over the same data could not do before reading everything.
Where helpers differ from array methods
The names match the array methods, the rules do not always:
// Where iterator helpers differ from their array namesakes.
function attempt(label, run) {
try {
console.log(`${label}:`, run());
} catch (error) {
console.log(`${label}: ${error.name}`);
}
}
attempt('reduce over nothing, no start value', () => [].values().reduce((a, b) => a + b));
attempt('reduce over nothing, start value 0', () => [].values().reduce((a, b) => a + b, 0));
attempt('take(-1)', () => [1, 2].values().take(-1));
attempt('array flatMap to strings', () => ['ab', 'cd'].flatMap((s) => s));
attempt('iterator flatMap to strings', () => ['ab', 'cd'].values().flatMap((s) => s).toArray());
attempt('iterator flatMap to arrays', () => ['ab', 'cd'].values().flatMap((s) => [...s]).toArray());
// One walk only: a helper shares its source, and a used helper is empty.
const source = [1, 2, 3].values();
const doubled = source.map((n) => n * 2);
console.log(source.next().value); // taken from the source, so doubled never sees it
console.log(doubled.toArray(), doubled.toArray());
console.log(JSON.stringify({ doubled: [1, 2].values().map((n) => n * 2) })); // an iterator is not data Output
reduce over nothing, no start value: TypeError
reduce over nothing, start value 0: 0
take(-1): RangeError
array flatMap to strings: [ 'ab', 'cd' ]
iterator flatMap to strings: TypeError
iterator flatMap to arrays: [ 'a', 'b', 'c', 'd' ]
1
[ 4, 6 ] []
{"doubled":{}}
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node helper-traps.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
reducewithout a start value throws aTypeErroron an empty iterator, as it does on an empty array. Pass the start value,reduce(fn, 0), whenever the input may be empty.takeanddropcheck their argument at once: a negative number orNaNis aRangeErrorwhen the method is called, not when the chain runs.flatMapflattens only what its callback returns as an iterator or an iterable object. The arrayflatMapkeeps a returned string as one element, while the iteratorflatMaprefuses strings with aTypeError. Return[...string], orIterator.from(string), to flatten characters.- A helper shares its source.
source.next()took the 1 away fromdoubled, which then saw only 2 and 3. And oncedoubledhas been read, it is empty:toArray()a second time gives[]. - An iterator is not data.
JSON.stringifywrites it as{}, so calltoArray()before storing or sending values.
Array methods or iterator helpers?
| Criterion | Array methods | Iterator helpers |
|---|---|---|
| Works on | arrays | any iterator; any iterable through Iterator.from |
| When the work happens | at once: each method over every element | on demand: one value at a time through the chain |
| Endless input | impossible | fine, with take, find or some to stop |
| Memory for the steps | a new array per step | one value in flight |
| Read again | any number of times | once; ask the source for a new iterator |
| Also available | length, indexes, sort, reverse, findLast, slice | only what the helpers offer; toArray() for the rest |
| When to choose | Small or finite data that you read several times, sort or index | Large, endless or expensive sequences, early exits and streaming |
For a short array that you filter once, the array methods are just as good and often clearer. Reach for helpers when the input is a generator, a stream of lines, or anything you might not want to read to the end.
Support
The helpers are part of ES2025. Following MDN’s browser-compat-data 8.1.4, Iterator.prototype.map and the other ten
methods, together with Iterator.from, are in Chrome 122, Firefox 131, Safari 18.4, Node.js 22.0.0, Deno 1.39 and
Bun 1.1.31, so every current major browser has them. The examples on this page ran on Node.js 24.21.0 and in
QuickJS (quickjs-emscripten 0.32.0), the engine of the Run buttons, with the same output. For an older runtime,
check typeof Iterator === 'function' && typeof Iterator.prototype.map === 'function' and fall back to the generator
functions of the previous lessons, which do the same work one yield at a time.
Exercises
Exercise · Medium · JavaScript
Rewrite three array pipelines with lazy iterator helpers
Each function in pipelines.mjs has its old array version in a comment. The array versions are correct for arrays, but Array.from never returns when the input is an endless generator, such as a live feed of payments. Rewrite each function with iterator helpers so that it works for any iterable, endless ones included, and reads no more of the input than its answer needs.
firstLarge(amounts, min, count)returns an array of the firstcountamounts that are at leastmin, each rounded to a whole number withMath.round.firstLongWord(words, minLength)returns the first word with at leastminLengthcharacters, orundefinedwhen a finite input has none.runningTotals(amounts)returns an iterator of running totals: for 5, 10 and 20 it gives 5, 15 and 35. It must return at once, before any total is computed, and compute each total only when asked.
Starter code · pipelines.mjs
/**
* The first `count` amounts of at least `min`, rounded to whole numbers.
* @param {Iterable<number>} amounts
* @param {number} min
* @param {number} count
* @returns {number[]}
*/
export function firstLarge(amounts, min, count) {
// Array version: return Array.from(amounts).filter((a) => a >= min).map((a) => Math.round(a)).slice(0, count);
return [];
}
/**
* The first word with at least `minLength` characters, or undefined.
* @param {Iterable<string>} words
* @param {number} minLength
* @returns {string | undefined}
*/
export function firstLongWord(words, minLength) {
// Array version: return Array.from(words).find((word) => word.length >= minLength);
return undefined;
}
/**
* An iterator of running totals, computed one at a time.
* @param {Iterable<number>} amounts
* @returns {Iterator<number>}
*/
export function runningTotals(amounts) {
// Array version:
// const totals = [];
// let total = 0;
// for (const amount of Array.from(amounts)) totals.push((total += amount));
// return totals.values();
return [].values();
} The sample tests · pipelines.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { firstLarge, firstLongWord, runningTotals } from './pipelines.mjs';
/** The first `count` values of an iterator, with next(). */
function first(iterator, count) {
const out = [];
while (out.length < count) {
const step = iterator.next();
if (step.done) break;
out.push(step.value);
}
return out;
}
/** A one-pass source of the given values (a generator, like a real feed). */
function* source(...values) {
yield* values;
}
/** An endless feed that repeats `values` and counts how many it has produced. */
function feed(values) {
const stats = { produced: 0 };
function* run() {
for (let i = 0; ; i++) {
stats.produced += 1;
yield values[i % values.length];
}
}
return { items: run(), stats };
}
test('firstLarge keeps the first large amounts, rounded', () => {
assert.deepEqual(firstLarge(source(120.4, 80, 250.6, 99.5, 300), 100, 2), [120, 251]);
assert.deepEqual(firstLarge(source(120.4, 80, 250.6), 100, 0), []);
assert.deepEqual(firstLarge([5, 50, 500.2], 40, 5), [50, 500]);
});
test('firstLarge on an endless feed reads only what it needs', () => {
const { items, stats } = feed([10, 99.6, 20, 150]);
assert.deepEqual(firstLarge(items, 50, 3), [100, 150, 100]);
assert.equal(stats.produced, 6);
});
test('firstLongWord finds the first long word, or nothing', () => {
assert.equal(firstLongWord(source('ok', 'iterator', 'helpers'), 5), 'iterator');
assert.equal(firstLongWord(['a', 'bb'], 3), undefined);
const { items, stats } = feed(['map', 'take', 'reduce', 'flatMap']);
assert.equal(firstLongWord(items, 6), 'reduce');
assert.equal(stats.produced, 3);
});
test('runningTotals adds up as it goes', () => {
assert.deepEqual(first(runningTotals([5, 10, 20]), 10), [5, 15, 35]);
assert.deepEqual(first(runningTotals([]), 10), []);
});
test('runningTotals is lazy', () => {
const { items, stats } = feed([1, 2, 3]);
const totals = runningTotals(items);
assert.equal(stats.produced, 0);
assert.deepEqual(first(totals, 4), [1, 3, 6, 7]);
assert.equal(stats.produced, 4);
}); A hint
Arrays have no helpers, but Iterator.from(iterable) gives you an iterator that has them for any input. take(count) stops after count values, find stops at the first match, and for runningTotals, keep the total in a variable outside the callback: Iterator.from(amounts).map((amount) => (total += amount)) returns at once.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
2 to 5 years · What is the difference between Array.prototype.map and Iterator.prototype.map?
Array.prototype.map is eager: it calls the callback for every element at once and returns a new array of the same
length. Iterator.prototype.map is lazy: it returns a new iterator and calls the callback only when a consumer asks
that iterator for its next value, one value at a time. So the iterator version works on endless sequences, holds no
intermediate array and does no work for values nobody reads, for example after take(3) or find. In exchange its
result is an iterator, not an array: it can be read once, has no length or indexes, and needs toArray() or a
spread to become an array. Arrays have no iterator map of their own, so you call it on array.values() or
Iterator.from(array). Both pass the callback a value and an index-like counter.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A search box filters 500,000 orders on every keystroke with orders.filter(matches).map(toRow).slice(0, 20). Would iterator helpers make it faster? Estimate the callback calls of each version when 10 % of orders match, and say what you would measure before changing anything.
-
Mid to senior
A pull request replaces arr.map(f) with arr.values().map(f).toArray() across the code base, claiming that lazy code is faster. Review it: which call sites can get faster, which change behaviour or break, and what would you ask the author for instead?
Key takeaways
- Iterator helpers live on
Iterator.prototype:map,filter,take,dropandflatMapreturn lazy iterators;reduce,toArray,forEach,some,everyandfindconsume. - A helper chain pulls one value at a time through every step and does no work until a consumer asks: the first three even squares cost 6 source values instead of a million.
- Arrays are not iterators: start with
array.values()orIterator.from(iterable), which also wraps strings and hand-made iterators. take,find,someandeveryclose the source withreturn()when they stop, so a generator’sfinallyruns.- Unlike the array version, iterator
flatMaprejects strings, a helper can be read only once, and an iterator is not data forJSON.stringify. - Use array methods for small data you index, sort or reread; use helpers for large, endless or expensive sequences.
References
- ECMAScript 2026 Language Specification: Properties of the Iterator Prototype Object (Ecma International)
- ECMAScript 2026 Language Specification: Iterator.from (Ecma International)
- ECMAScript 2026 Language Specification: Iterator.prototype.flatMap (Ecma International)
- Iterator helpers proposal (Ecma International (TC39))
- Iterator (MDN Web Docs)
- @mdn/browser-compat-data 8.1.4: javascript/builtins/Iterator.json (MDN (browser-compat-data))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress