Iterator helpers
Build lazy JavaScript data pipelines with Iterator.from, map, filter, take, drop, flatMap, consuming helpers, and feature-detected newer iterator proposals.
- 01Build lazy pipelinesTrace how iterator helpers pull one value at a time and stop early.
- 02Choose a terminal helperKnow when a chain returns another iterator and when it consumes the source.
- 03Ship defensivelyUse Iterator.from, understand one-shot consumption, and feature-detect newer helpers.
Lazy helpers for any iterator
Iterator helpers bring familiar array-method names to values that are produced over time. If you know map, filter, and reduce on arrays, the names will feel friendly. The big difference is timing: iterator helpers are usually lazy. They do not run the whole source immediately. They wait until something asks for the next value.
The core standardized helpers are Iterator.from, map, filter, take, drop, flatMap, reduce, toArray, forEach, some, every, and find. They are supported in modern browsers and Node 22; feature-detect for older environments.
Transforming helpers return another iterator. Consuming helpers pull values and return a final result. Once values are pulled, a one-shot iterator does not rewind.
Read one shopping-list item, check it, then move to the next. If you only need three items, stop there. An array chain often prepares a full list before moving to the next step.
- In real life: Read one item on the list
- In JavaScript: One value reaches
map - In real life: Check that same item
- In JavaScript: The mapped value reaches
filter - In real life: Stop after enough items
- In JavaScript:
take(3)stops pulling the source - In real life: Write every item before shopping
- In JavaScript: Array methods usually build whole intermediate arrays
Where the analogy stops: A paper list does not create items. A JavaScript iterator pauses between pulls; no helper callback runs until the next value is requested.
You will compare lazy and eager chains, adapt a plain next() object, play with every helper, feature-detect newer proposals, sort realistic tasks, then practice with predictions and bugs.
Iterator.from adapts next() objects
ADAPTERAn iterator is any object with a next() method that returns { value, done } records. That protocol is powerful, but a hand-rolled iterator does not automatically have helper methods. Iterator.from(value) is the adapter: it returns an iterator object that can use helper methods.
The source already knows how to produce values. It just lacks the new outlet shape. Iterator.from fits that source into the helper-method world.
- In real life: A travel adapter accepts the plug you already own
- In JavaScript:
Iterator.fromaccepts an iterator-like object - In real life: The wall side now has the right shape
- In JavaScript: The returned object has helper methods
- In real life: The adapter does not create electricity
- In JavaScript: It still pulls values from the original source
Where the analogy stops: An adapter plug can be reused physically. Many iterators are one-shot: after the original source is drained, wrapping it again does not restore spent values.
const handRolled = { current: 0, next() { this.current += 1; return this.current <= 3 ? { value: this.current, done: false } : { value: undefined, done: true }; },}; const result = Iterator.from(handRolled) .map(n => n * 10) .toArray(); console.log(result);The output is [10, 20, 30]. The object on lines 1–9 only has next(). After line 11 adapts it, map and toArray are available. This matters for generators, custom data readers, and protocol-based APIs you will meet in the async iteration lesson.
The lazy conveyor belt
INTERACTIVERead the lazy chain first. numbers() logs whenever the source is pulled. The map and filter callbacks log too. With take(3), the chain should stop after three passing values, not after the whole source is exhausted.
function* numbers() { for (const value of [1, 2, 3, 4, 5, 6]) { console.log("pull " + value); yield value; }} const result = numbers() .map(n => { console.log("map " + n); return n * 2; }) .filter(n => { console.log("filter " + n); return n % 3 !== 0; }) .take(3) .toArray(); console.log(result.join(", "));- pull 1
- map 1
- filter 2
- pull 2
- map 2
- filter 4
- pull 3
- map 3
- filter 6
- pull 4
- map 4
- filter 8
- result 2, 4, 8
The lazy chain pulled 4 source values, ran 8 callbacks, and stopped as soon as take(3) had enough.
Switch to the eager array version. The final result still starts with 2, 4, 8, but the callback count changes. Array map handles all six numbers before array filter starts. The lazy iterator does pull → map → filter for one value at a time and stops after the fourth source value because three values have passed.
Choose lazy or eager, then step. Watch whether one item moves through every station before the next item starts.
script
function* numbers() { for (const value of [1, 2, 3, 4, 5, 6]) { console.log("pull " + value); yield value; }} .map(n => { console.log("map " + n); return n * 2; }) .filter(n => { console.log("filter " + n); return n % 3 !== 0; }) .take(3) .toArray(); console.log(result.join(", "));The standardized helpers in this lesson are available in current Chrome, Firefox, Safari, and Node 22. Libraries and long-lived apps should still feature-detect because older browsers may be present.
map, filter, take, drop and flatMap
The transforming helpers return new iterator objects. They do not produce a visible result until something consumes them.
| Helper | Lazy? | Returns | Common use |
|---|---|---|---|
map(callback) | Yes | Iterator | Transform each item as it is pulled |
filter(callback) | Yes | Iterator | Keep values whose test is truthy |
take(n) / drop(n) | Yes | Iterator | Limit or skip a prefix; n must be a non-negative number |
flatMap(callback) | Yes | Iterator | Replace each item with zero or more yielded values |
toArray() | Consumes | Array | Materialize the remaining values |
reduce(callback, initial) | Consumes | One value | Accumulate the whole remaining iterator |
some / every / find | Consumes until decided | Boolean or value | Short-circuit searches |
forEach(callback) | Consumes | undefined | Run a side effect for each remaining item |
map transforms each pulled value. filter keeps values when its callback returns truthy. take(n) lets the first n remaining values through; drop(n) skips the first n. Both validate their count. Negative values and NaN throw RangeError instead of silently doing something surprising.
flatMap is the one-to-many helper. Its callback returns an iterable, and the helper yields each inner value. Use it for things like splitting lines into words, turning a calendar day into zero or more events, or expanding a page of API results into individual rows.
const iterator = Iterator.from([1, 2, 3]);console.log(iterator.take(2).toArray());console.log(iterator.toArray());The first log prints [1, 2]. The second log prints [3], because the original iterator has already yielded the first two values. If you need to iterate twice, keep the original data (for example an array) or create a fresh iterator.
some, every, find, reduce, forEach and toArray
Consuming helpers are terminals. They pull values from the chain and return something other than another lazy iterator.
toArray()materializes every remaining value into an array.reduce(callback, initial)combines all remaining values into one accumulator.forEach(callback)runs a side effect and returnsundefined.some,every, andfindshort-circuit when the answer is known.
Short-circuiting is where laziness often shines. find does not care about later values once a match is found. some stops at the first truthy callback result, and every stops at the first falsy result. In an infinite sequence, those methods are safe only when the condition can eventually decide.
Iterator.from([1, 2, 3]).map(n => n * 2) describes work. It does not print or build an array. Add toArray(), find(...), reduce(...), or a for...of loop when you actually want values.
Helper playground
INTERACTIVETry a source, toggle helpers, then choose a terminal helper. The infinite counter is guarded: the playground refuses combinations that would try to consume forever. This is not a toy-only concern. Real streams, cursors, and generated sequences need the same discipline.
function* counter() { let value = 1; while (true) yield value++;} const result = Iterator.from(source) .drop(1) .map(n => n * 2) .filter(n => n % 3 !== 0) .take(4) .toArray();- chain
array → map(n => n * 2) → filter(n => n % 3 !== 0) → take(6) → toArray()result[2, 4, 8, 10]
Result: [2, 4, 8, 10]. Chain: array → map(n => n * 2) → filter(n => n % 3 !== 0) → take(6) → toArray().
Notice how Set, arrays, generators, and infinite counters can share the same helper vocabulary once they are iterators. That is the practical payoff of the iteration protocol introduced in the previous lessons.
Iterator.concat, Iterator.zip, zipKeyed and proposals
FEATURE DETECTAt the time of writing, TC39 lists Sync Iterator helpers, Joint Iteration, and Iterator Sequencing as finished proposals. Those cover the standardized helper set plus ideas such as combining iterables, zipping them position-by-position, and sequencing iterables. Node 22 currently exposes the core helper methods but not Iterator.concat, Iterator.zip, or Iterator.zipKeyed. Treat finished-but-new APIs as feature-detected APIs.
// At the time of writing, Node 22 does not expose these yet.// Feature-detect before using them in production code.const canConcat = typeof Iterator.concat === "function";const canZip = typeof Iterator.zip === "function";const canZipKeyed = typeof Iterator.zipKeyed === "function"; console.log({ canConcat, canZip, canZipKeyed });TC39 also lists iterator chunking, Iterator Includes, and Iterator Join as active Stage 3 proposals. The chunking proposal is where chunk-like and window-like operations are being explored. Until those names are actually present in your target runtimes, write small generator utilities or use a library, and label them as sketches rather than standard JavaScript.
function* chunks(iterable, size) { let chunk = []; for (const value of iterable) { chunk.push(value); if (chunk.length === size) { yield chunk; chunk = []; } } if (chunk.length) yield chunk;}// At the time of writing, Node 22 does not expose these yet.// Feature-detect before using them in production code.const canConcat = typeof Iterator.concat === "function";const canZip = typeof Iterator.zip === "function";const canZipKeyed = typeof Iterator.zipKeyed === "function"; console.log({ canConcat, canZip, canZipKeyed });function* chunks(iterable, size) { let chunk = []; for (const value of iterable) { chunk.push(value); if (chunk.length === size) { yield chunk; chunk = []; } } if (chunk.length) yield chunk;}Standard iterator helpers
- from: not detected
- map: not detected
- filter: not detected
- take: not detected
- drop: not detected
- flatMap: not detected
- reduce: not detected
- toArray: not detected
- forEach: not detected
- some: not detected
- every: not detected
- find: not detected
Newer or proposal helpers
- concat: not detected
- zip: not detected
- zipKeyed: not detected
- chunks: not detected
- windows: not detected
- includes: not detected
- join: not detected
Feature detection runs after mount to avoid server/browser mismatches.
Where you'll use this
Iterator helpers are useful when the source is not naturally a small array or when stopping early saves work. Think log readers, generated IDs, pagination cursors, file lines, database-like cursors in a local tool, and data transformations that should stay readable without allocating every intermediate array.
- Find the first error line in a huge log
- Show the next 20 items from an infinite counter
- Sum every invoice in a finished monthly export
- Render every search result already loaded in memory
- Drop the first 100 sensor readings, then show 10
- Sort products by price before taking page one
Sort each realistic task by the style that fits best.
Eager is not bad. If you already have a small array and need every value, array methods are often clearer. Laziness is a tool for composition, memory use, and early stopping, not a rule that replaces arrays.
Common misconceptions
- “map runs immediately.” Iterator
mapreturns a lazy iterator. A terminal pulls it. - “Iterator helpers rewind like arrays.” Most iterators are one-shot. Consumed values are gone.
- “take(-1) means take none.” Invalid counts throw
RangeError. - “toArray is always harmless.” On an infinite source it never finishes. Limit or short-circuit first.
- “Finished proposal means everywhere today.” Feature-detect newer names such as
Iterator.zip. - “Lazy is always faster.” Sorting, grouping, and full reductions still need all relevant data.
| Question | Iterator helpers | Array methods |
|---|---|---|
| When do callbacks run? | When a consumer pulls values | During the method call |
| Can the source be infinite? | Yes, if a later helper stops | No, arrays are finite materialized collections |
| Can you reuse the same result? | Usually no; it is one-shot | Yes; arrays keep their elements |
| How do you get an array? | Call toArray() | You already have or receive an array |
Practice exercises
4 EXERCISESRun the program mentally. Type the exact text printed by line 10.
function* ids() {
yield 1;
yield 2;
yield 3;
}
const result = ids()
.map(id => id * 10)
.take(2)
.toArray();
console.log(result.join("|"));console.log("10|20");The iterator yields 1 and 2, maps them to 10 and 20, then take(2) stops the chain.
What does the second console.log print?
const values = Iterator.from(["a", "b", "c"]);
console.log(values.take(2).toArray().join(""));
console.log(values.toArray().join(""));// second log prints cAfter take(2) pulls a and b, only c remains in the original iterator.
The code prints an iterator object, not a number. Fix it so it prints the first even value above 6.
const firstLarge = Iterator.from([2, 5, 8, 11])
.filter(n => n > 6);
console.log(firstLarge);const firstLarge = Iterator.from([2, 5, 8, 11])
.filter(n => n > 6)
.find(n => n % 2 === 0);
console.log(firstLarge);Adding find(n => n % 2 === 0) consumes only until the first matching value, so it prints 8.
Predict the total printed by this terminal helper chain.
const total = Iterator.from([3, 4, 5])
.map(n => n * 2)
.reduce((sum, n) => sum + n, 0);
console.log(total);const total = Iterator.from([3, 4, 5])
.map(n => n * 2)
.reduce((sum, n) => sum + n, 0);
console.log(total);The mapped values are 6, 8, and 10. The reducer adds them to 24.
Check your understanding
7 QUESTIONSQuestion 1 of 7Which statement best describes iterator helper laziness?
Choose an answer to see the explanation.
Question 2 of 7What does this one-shot iterator example print second?
Read the code, then predictconst it = Iterator.from([1, 2, 3]); console.log(it.take(2).toArray().join(",")); console.log(it.toArray().join(","));Choose an answer to see the explanation.
Question 3 of 7What does
takedo with an invalid negative count?Read the code, then predicttry { Iterator.from([1]).take(-1).next(); } catch (error) { console.log(error.name); }Choose an answer to see the explanation.
Question 4 of 7Which helper consumes an iterator and returns an array?
Choose an answer to see the explanation.
Question 5 of 7What does this short-circuiting search print?
Read the code, then predictlet calls = 0; const found = Iterator.from([2, 4, 6, 9, 10]).find(n => { calls += 1; return n % 2 === 1; }); console.log(found + ":" + calls);Choose an answer to see the explanation.
Question 6 of 7At the time of writing, how should you treat
Iterator.zipandIterator.concat?Choose an answer to see the explanation.
Question 7 of 7Which task is the best fit for a lazy iterator pipeline?
Choose an answer to see the explanation.
Key takeaways
Iterator.fromadapts iterator-like values so helper methods can be used.map,filter,take,drop, andflatMapare lazy and return iterators.toArray,reduce,forEach,some,every, andfindconsume the remaining iterator.- One-shot iterators do not rewind; create a fresh iterator if you need another pass.
- Feature-detect newer names such as
Iterator.zip, chunking, includes, and join before relying on them.
Iterator helpers are lazy method-based tools for adapting, transforming, limiting, searching, and consuming values from the iterator protocol.
Up next: Async iteration & async generators.