← All posts

4 min read

Iterator helpers: lazy map and filter without the arrays

Array chains copy at every step. Iterator helpers run lazily, work on infinite sources, and close generators on early exit. Tested in Chromium and Node.

Array methods are eager: every .map() and .filter() allocates a full intermediate array. Iterator helpers give you the same vocabulary, but lazy, one element at a time, and they work on infinite sources. This post shows where they beat arrays, the closing behavior that makes them safe, and the sharp edges I hit while testing them in Chromium 153 and Node 22.

Lazy pipelines in four lines

// Lazy: only as much work as `take(3)` needs
function* ids() { let n = 1; while (true) yield n++; }

const squares = ids()
  .filter((n) => n % 2 === 0)
  .map((n) => n * n)
  .take(3)
  .toArray(); // [4, 16, 36]

An array version of this is impossible, because ids() never ends. With helpers, I logged the callbacks and got f1,f2,m2,f3,f4,m4,f5,f6,m6: each value flows through the whole chain before the next one is pulled, and nothing runs past the sixth element.

Iterator helpers became Baseline Newly available on March 31, 2025, with support in all three major engines. MDN lists the map() page as Baseline 2025, "since March 2025".

Where it beats arrays

The benefit is not syntax, it is skipped work. Take a 1000-element array and ask for the first three odd values:

const arr = Array.from({ length: 1000 }, (_, i) => i);

let calls = 0;
arr.map((x) => { calls++; return x; }).filter((x) => x % 2).slice(0, 3);
// calls === 1000

calls = 0;
arr.values().map((x) => { calls++; return x; }).filter((x) => x % 2).take(3).toArray();
// calls === 6

The array version ran the map callback 1000 times. The iterator version ran it 6 times. If the callback is expensive (parsing, formatting, a DOM read), that difference is the whole point.

Call .values(), .keys() or .entries() to get an iterator from an array, Map or Set. For a hand-written iterator object that does not inherit from Iterator.prototype, wrap it in Iterator.from():

const counter = {
  i: 0,
  next() {
    return this.i < 3 ? { value: this.i++, done: false } : { value: undefined, done: true };
  },
};

Iterator.from(counter).map((x) => x * 10).toArray(); // [0, 10, 20]

Maps and Sets are a natural fit, since you skip the [...map].filter(...) copy:

const prices = new Map([["a", 1], ["b", 2], ["c", 3]]);
prices.entries().filter(([, v]) => v > 1).map(([k]) => k).toArray(); // ["b", "c"]

Early exit closes the source

Generators often hold resources: a file handle, a cursor, a lock. The helpers call return() on the underlying iterator when they stop early, so finally blocks run. I tested this with a generator that sets a flag in finally:

let closed = false;
function* source() {
  try { let i = 0; while (true) yield i++; }
  finally { closed = true; }
}

source().take(2).toArray();
console.log(closed); // true

closed = false;
source().find((x) => x === 3);
console.log(closed); // true

closed = false;
try { source().map(() => { throw new Error("boom"); }).next(); } catch {}
console.log(closed); // true

All three printed true in Chromium 153: take() finishing, find() short-circuiting, and a callback throwing. MDN documents that helper objects implement return() and [Symbol.dispose](), which forwards to the underlying iterator and makes them usable with using.

Gotcha 1: iterators are one-shot

A helper shares its source with the original iterator, and there is no way to fork it. MDN calls this out, and my test matched:

const it = [1, 2, 3].values();
const it2 = it.drop(0); // looks like a copy, is not

[it.next().value, it2.next().value, it.next().value]; // [1, 2, 3]

Both objects drain the same stream, so the values interleave. A consumed helper is also empty: calling toArray() twice on the same .map() result gives [] the second time. If you need two passes, collect with toArray() first, or build the pipeline from a function that returns a fresh iterator.

Gotcha 2: flatMap does not accept strings

Array flatMap happily treats a returned string as a value. The iterator version requires the callback to return an iterable or iterator object, and rejects primitives:

[1].values().flatMap(() => "ab").toArray(); // TypeError
[1].values().flatMap(() => ["a", "b"]).toArray(); // ["a", "b"]

Return an array (or [..."ab"]) if you mean to flatten characters.

Joining sources with Iterator.concat

Iterator.concat() is newer. MDN lists it as Baseline 2026, "Newly available" since March 2026. It lazily chains any iterables, like Array.prototype.concat without the copy:

const merged = [
  ...Iterator.concat([1, 2], new Set([3]), (function* () { yield 4; })()),
];
// [1, 2, 3, 4]

const map = new Map(Iterator.concat(map1, map2, map3));

It worked in Chromium 153. It is not present in Node 22.23 (typeof Iterator.concat is "undefined"), while the basic helpers (map, filter, take, toArray) are. Check your Node version, or use a polyfill such as core-js or es-iterator-helpers, which MDN lists.

Arguments must be iterable, so wrap a bare iterator object with Iterator.from() first. Also, per MDN, anything after an infinite iterable is never reached, and the argument list should be small. For many inputs, use flatMap() on an iterator of iterables.

What not to use yet

MDN's Iterator page now lists chunks() and windows() for batching and sliding windows, but the chunks() page is marked Experimental and describes the Iterator Chunking proposal. In Chromium 153 and Node 22, typeof Iterator.prototype.chunks is "undefined". Do not ship them without a polyfill.

Takeaway

  • Use iterator helpers when the source is large, infinite, or expensive per item and you only need a prefix. In my test, 6 callback calls instead of 1000.
  • Start from .values(), .entries() or a generator, end with .toArray(), .find(), .reduce() or a for...of.
  • Early exits (take, find, a thrown error) close the underlying generator, so finally cleanup runs.
  • Remember they are single-use and flatMap wants iterables, not strings.
  • The core helpers are Baseline since March 2025. Iterator.concat is Baseline 2026, so verify your Node runtime. chunks() and windows() are still experimental.