Iterator helpers
Build lazy JavaScript data pipelines with Iterator.from, map, filter, take, drop, flatMap, consuming helpers, and feature-detected newer iterator proposals.
- 01Follow a lazy chainTrace how a chain of iterator helpers pulls one value at a time and stops early.
- 02Know which helper ends the chainTell a helper that gives back another iterator from one that pulls the values out and gives a result.
- 03Use the helpers safelyUse Iterator.from, remember that an iterator can be used only once, and check that newer helpers exist before you use them.
Array-style helpers for iterators
An iterator is an object that gives you values one at a time. Each time you call its next() method, you get the next value. Generators give you iterators, and so do methods such as entries() on a Map or a Set.
Iterator helpers are methods you can call on an iterator. Many have the same names as array methods, such as map and filter. If you know map, filter, and reduce on arrays, the names will look familiar.
The big difference is when the work happens. An array method is eager: it does its work right away, on every item. Most iterator helpers are lazy: they do no work until someone asks for the next value. Asking an iterator for its next value is called pulling a value.
These are the standard helpers this lesson covers:
Iterator.fromgives you an iterator that has the helpers.map,filter,take,drop, andflatMapgive back a new iterator. They wait.toArray,reduce,forEach,some,every, andfindpull the values out and give back a result.
They work in current browsers and in Node 22. Older browsers may not have them, so code for those browsers should check first.
Helpers like map give back another iterator and wait. Helpers like toArray pull the values and give back a final result. This lesson calls them consuming helpers, because they use up the values. Once a value is pulled, the iterator does not go back to it.
You read one item on your shopping list. You find it and put it in your basket. Only then do you read the next item. If you only need three things, you stop after three. You never copy out the whole list first.
- In real life: Read one item on the list
- In JavaScript: Pull one value from the source
- In real life: Find that item before you read the next one
- In JavaScript: That one value goes through
map, thenfilter - In real life: Stop once you have three items
- In JavaScript:
take(3)stops pulling - In real life: Copy out the whole list before you start
- In JavaScript: An array method works on every item first
Iterator.from adds the helpers
ADAPTERAn iterator is any object with a next() method. Each call to next() returns a small object shaped like { value, done }. The value part is the next value. The done part is true when there are no more values.
An object you write by hand with only a next() method does not get the helpers. It has no map and no toArray. The fix is Iterator.from(value). It gives back an iterator that pulls from your object and also has every helper.
In another country, your charger plug does not fit the wall socket. A travel adapter fixes the shape. Your charger works the same as before; now it also fits.
- In real life: You plug your own charger into a travel adapter
- In JavaScript: You pass your object to
Iterator.from - In real life: The adapter fits the wall socket
- In JavaScript: The result has the helper methods
- In real life: The adapter makes no power of its own
- In JavaScript: The values still come from your object
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);It prints [10, 20, 30]. Line by line:
- Lines 1–9 make
handRolled. It gives 1, 2, 3, then saysdone: true. It has onlynext(). - Line 11 passes it to
Iterator.from, which gives back an iterator with the helpers. - Line 12 multiplies each value by 10.
- Line 13 calls
toArray(), which pulls every value and puts them in an array.
handRolled can only give values with next(). Iterator.from wraps it in an iterator that also has map() and toArray().You can do the same with any object that gives values through next(), such as a reader you wrote for your own data.
const prices = new Map([ ["tea", 3], ["bread", 4], ["rice", 8],]); const labels = Iterator.from(prices.entries()) .map(([name, price]) => name + ":" + price) .toArray(); console.log(labels.join(", "));prices.entries() already returns an iterator. Each value it gives is a [name, price] pair. Built-in iterators like this one already have the helpers, so Iterator.from just hands the same iterator back.
- Line 8 turns one pair at a time into text like
"tea:3". - Line 9 calls
toArray()to collect the results. - Line 11 prints
tea:3, bread:4, rice:8.
No extra array of pairs is made along the way.
Use an iterator when the values come one at a time. Use an array when you need to:
- jump to an item by its position, like
list[2], - go through the values more than once, or
- have all the values right now.
A lazy chain handles one value at a time
INTERACTIVEA generator is a function that makes values one at a time, only when asked. Calling a generator gives you an iterator, so you can use the helpers on it straight away. The generator below counts up forever and gives out only the even numbers.
function* evenNumbers() { let value = 0; while (true) { if (value % 2 === 0) yield value; value += 1; }} const firstThree = evenNumbers().take(3).toArray();console.log(firstThree);Line 9 asks for only three values, so it gets [0, 2, 4]. After the third value, take(3) stops pulling, and the endless loop is never asked again. Without take(3), toArray() would wait forever, because this generator never says done: true.
The tool below runs a longer chain. A function you pass to a helper, like n => n * 2, is called a callback. Each part of the chain logs a line when it runs:
pullmeans the source gave one value.mapmeans themapcallback ran on that value.filtermeans thefiltercallback checked the result.
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 iterator chain pulled 4 source values, made 8 callback calls, and stopped as soon as take(3) had enough.
Switch to the array version. The result is still 2, 4, 8, but the log changes:
- The array chain runs
mapon all six numbers. Only then doesfilterstart. - The iterator chain pulls one value, maps it, and filters it. Then it pulls the next one.
- The iterator chain stops after the fourth value, because three values have passed the filter by then.
2, 4, 8. The iterator chain takes one value through every step, then stops after 4 values. The array chain does each step for all 6 values.Pick lazy or array, then step. Watch one value go through every step before the next value 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 standard helpers in this lesson work in current Chrome, Firefox, Safari, and Node 22. Some people still use older browsers. If your code must run there too, check that a helper exists before you use it.
map, filter, take, drop and flatMap
These five helpers each give back a new iterator. Nothing happens until something pulls from it. The table also lists the consuming helpers from the next section, so you can compare them. Read each row as: the helper, whether it waits to be asked, what it gives back, and what it is for.
| Helper | Does it wait to be asked? | What it gives back | What it is for |
|---|---|---|---|
map(callback) | Yes. It runs only when a value is pulled. | A new iterator. | Change each value. |
filter(callback) | Yes. It runs only when a value is pulled. | A new iterator. | Keep values where the callback's answer counts as true. |
take(n) / drop(n) | Yes. It runs only when a value is pulled. | A new iterator. | Keep or skip the first n values. n can't be negative. |
flatMap(callback) | Yes. It runs only when a value is pulled. | A new iterator. | Turn each value into zero or more values. |
toArray() | No. It pulls every value. | An array. | Collect the remaining values. |
reduce(callback, initial) | No. It pulls every value. | One value. | Combine all remaining values into one. |
some / every / find | No. It pulls until it knows the answer. | true or false, or the value it found. | Search, and stop early. |
forEach(callback) | No. It pulls every value. | undefined. | Do something with each value, like logging it. |
map(callback)changes each value it pulls.filter(callback)keeps a value when the callback returns a truthy value, which is a value that counts as true.take(n)lets the firstnvalues through, then stops.drop(n)skips the firstnvalues and lets the rest through.
take and drop check their number. A negative number or NaN throws a RangeError, which is an error for a number outside the allowed range. They do not quietly guess what you meant.
flatMap is for when one value should become several values, or none. Its callback returns something you can loop over, like an array. The helper then gives out each item inside it, one by one.
const baskets = [["tea", "bread"], ["rice"]];const items = Iterator.from(baskets) .flatMap(basket => basket) .toArray(); console.log(items.join(", "));Each basket is an array, so flatMap can loop over it. It gives out tea, then bread, then rice, and line 6 prints tea, bread, rice. If the callback returned a number instead, flatMap would throw a TypeError, because you can't loop over a number.
You can use flatMap to split lines into words, to turn one calendar day into its events, or to turn one page of results into single rows.
An iterator remembers where it is. Once it gives out a value, it never gives that value again. An iterator that can be used only once like this is called one-shot.
const iterator = Iterator.from([1, 2, 3]);console.log(iterator.take(2).toArray());console.log(iterator.toArray());Line 2 prints [1, 2]. Line 3 prints [3], because line 2 already pulled 1 and 2 from the same iterator. To go through the values again, keep the original array, or make a fresh iterator.
1 and 2. Line 3 asks the same iterator again, so it starts at 3.Some sources keep something open, such as a file or a database connection. They can have a return() method that cleans up. When a helper stops early, it calls that method on the source.
function makeReader(log) { let value = 1; return { next() { return { value: value++, done: false }; }, return() { log.push("closed"); return { value: undefined, done: true }; }, [Symbol.iterator]() { return this; }, };} const log = [];const values = Iterator.from(makeReader(log)).take(2).toArray();console.log(values);console.log(log);The reader on lines 1–13 counts 1, 2, 3, and so on forever. Its return() method on lines 7–10 adds "closed" to the log. When take(2) has its two values, it calls return() before the chain finishes. So line 17 prints [1, 2] and line 18 prints ["closed"]. A real source can close its file or connection at that moment.
some, every, find, reduce, forEach and toArray
Consuming helpers pull values out of the chain. They give back a final result, not another iterator.
toArray()puts every remaining value into an array.reduce(callback, initial)combines every remaining value into one value, starting frominitial.forEach(callback)calls the callback for each value, for example to log it. It returnsundefined.some,every, andfindstop as soon as they know the answer.
let checks = 0;const firstOdd = Iterator.from([2, 4, 7, 8]).find(value => { checks += 1; return value % 2 === 1;}); console.log(firstOdd + ":" + checks);find checks 2, then 4, then 7. 7 is odd, so it stops there. Line 7 prints 7:3: the value found, then the number of checks. The callback never sees 8.
find checks one value at a time. It stops at 7, the first odd value, so it prints 7:3.Stopping early helps most with long or endless sources:
findstops at the first value that matches.somestops at the first value where the callback returns a truthy value.everystops at the first value where the callback returns a falsy value, one that counts as false.
On an endless source, use these only when the answer will come at some point. Otherwise they keep checking forever.
Iterator.from([1, 2, 3]).map(n => n * 2) only describes the work. It prints nothing and makes no array. To get values, end the chain with toArray(), find(...), or reduce(...), or loop over it with for...of.
Helper playground
INTERACTIVEPick a source, turn helpers on or off, then pick a consuming helper. The infinite counter counts 1, 2, 3, and so on forever. The playground refuses any chain that would pull from it forever. Real code needs the same care with endless sources.
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(6) .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().
An array, a Set, a generator, and the endless counter all use the same helpers. Each of them gives an iterator, and the helpers work on any iterator.
Newer helpers: concat, zip, and proposals
FEATURE DETECTNew JavaScript features are planned by a group called TC39. Each planned feature is called a proposal. A proposal moves through numbered stages, and stage 4 means finished. At the time of writing, TC39 lists these as finished:
- Iterator helpers: the standard helpers in this lesson.
- Joint Iteration:
Iterator.zipandIterator.zipKeyed, which pair up values from several iterators by position. - Iterator Sequencing:
Iterator.concat, which joins several iterators one after another.
Finished does not mean every browser or Node version has it yet. Node 22 has the standard helpers, but not Iterator.concat, Iterator.zip, or Iterator.zipKeyed. So check that a newer helper exists before you use it. This check is called feature detection.
// 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 });Lines 3–5 use typeof, which gives the type of a value as text. A method that exists gives "function". In Node 22, all three checks are false.
TC39 also lists three ideas at stage 3, one step before finished:
- Iterator chunking: splitting values into groups, called chunks, or into sliding windows.
- Iterator Includes: checking whether an iterator gives a certain value.
- Iterator Join: joining the values into one string.
Until the places your code runs have these, write a small generator yourself or use a library. Make it clear that it is your own code, not 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;}- Line 1 starts a generator that takes anything you can loop over, plus a group size.
- Lines 3–4 add each value to the current group, called
chunk. - When the group reaches
sizeitems, line 6 gives it out and line 7 starts a new one. - Line 10 gives out the last, shorter group, if there is one.
// 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 found
- map: not found
- filter: not found
- take: not found
- drop: not found
- flatMap: not found
- reduce: not found
- toArray: not found
- forEach: not found
- some: not found
- every: not found
- find: not found
Newer and proposed helpers
- concat: not found
- zip: not found
- zipKeyed: not found
- chunks: not found
- windows: not found
- includes: not found
- join: not found
The check runs once the page has loaded in your browser.
Where you'll use this
Iterator helpers help when the source is not a small array, or when stopping early saves work. For example:
- reading lines from a big log file,
- making IDs one at a time,
- loading results one page at a time,
- reading rows from a local database.
They also keep a chain easy to read without making a new array at every step.
- Find the first error line in a huge log
- Show the next 20 numbers from an endless counter
- Add up every invoice in a finished monthly list
- Show every search result that is already loaded
- Skip the first 100 sensor readings, then show 10
- Sort products by price before showing page one
Put each task under the style that fits it best.
Eager is not bad. If you already have a small array and need every value, array methods are often clearer. Use lazy helpers when you want to stop early, save memory, or work with an endless source.
Common misconceptions
- “map runs right away.” Iterator
mapgives back a new iterator and waits. A consuming helper pulls the values. - “Iterators go back to the start, like arrays.” Most iterators are one-shot. A value that was pulled is gone.
- “take(-1) means take none.” A negative count throws a
RangeError. - “toArray is always safe.” On an endless source, it never finishes. Add
takefirst, or usefindorsome. - “A finished proposal works everywhere today.” Some places don't have it yet. Check for newer names such as
Iterator.zipfirst. - “Lazy is always faster.” Sorting, grouping, and adding up every value still need all the values.
The table compares the two styles. Read each row as a question, then the answer for iterator helpers and for array methods.
| Question | Iterator helpers | Array methods |
|---|---|---|
| When do the callbacks run? | When a consuming helper pulls a value. | Right away, while the method runs. |
| Can the source be endless? | Yes, if a later helper stops it. | No. An array always has a fixed number of items. |
| Can you use the result twice? | Usually not. It is one-shot. | Yes. An array keeps its items. |
| How do you get an array? | Call toArray(). | You already have one. |
Practice exercises
4 EXERCISESRun the program in your head. Type the exact text that line 12 prints.
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 generator gives 1 and 2. map turns them into 10 and 20. Then take(2) stops the chain, and line 12 prints the two values joined with |.
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 ctake(2) pulls a and b on line 2. Only c is left for line 3.
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);find(n => n % 2 === 0) pulls values only until the first even one, so it prints 8.
Predict the total that this chain prints.
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. reduce adds them: 0 + 6 + 8 + 10 = 24.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does it mean that iterator helpers are lazy?
Choose an answer to see the explanation.
Question 2 of 7This code uses one iterator twice. What does the second log print?
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 a 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 pulls the values out and gives back an array?
Choose an answer to see the explanation.
Question 5 of 7What does this 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 use
Iterator.zipandIterator.concat?Choose an answer to see the explanation.
Question 7 of 7Which task fits a lazy iterator chain best?
Choose an answer to see the explanation.
Key takeaways
Iterator.fromgives the helpers to an object that only hasnext().map,filter,take,drop, andflatMapare lazy. They give back a new iterator and wait.toArray,reduce,forEach,some,every, andfindpull the remaining values and give back a result.- An iterator does not go back to the start. Make a fresh one if you need the values again.
- Check that newer helpers, such as
Iterator.zip, exist before you use them.
Iterator helpers are methods for iterators. Most of them wait, and work on one value at a time, only when a value is pulled.
Up next: Async iteration & async generators.