EdgeCases Logo
Mar 2026
JavaScript
Deep
7 min read

JavaScript Iterator Helpers: Lazy .map(), .filter(), and .take()

Lazy .map(), .filter(), and .take() for iterators — and the gotchas

javascript
iterators
es2025
lazy-evaluation
performance
generators

Array methods are eager — [1,2,3].map(x => x * 2).filter(x => x > 2) creates two intermediate arrays before you even use the result. ES2025's iterator helpers bring .map(), .filter(), .take(), and friends to iterators with lazy evaluation. The performance implications are significant, but the edge cases around infinite sequences and consumption will trip you up.

The Core API

Iterator helpers extend Iterator.prototype with chainable methods that return new iterators:

// All methods available on iterators
.map(fn)       // Transform each value
.filter(fn)    // Keep values where fn returns true
.take(n)       // Yield first n values, then stop
.drop(n)       // Skip first n values, yield rest
.flatMap(fn)   // Map + flatten one level
.reduce(fn, initial)  // Accumulate to single value
.some(fn)      // True if any value passes
.every(fn)     // True if all values pass
.find(fn)      // First value that passes
.toArray()     // Collect into array

Unlike array methods, you can't call these directly on arrays. Convert first:

// Option 1: .values() for arrays
const result = [1, 2, 3]
  .values()
  .map(x => x * 2)
  .toArray();

// Option 2: Iterator.from() for any iterable
const set = new Set([1, 2, 3]);
const result = Iterator.from(set)
  .filter(x => x > 1)
  .toArray();

Lazy Evaluation: The Key Difference

Array methods execute immediately and return complete arrays. Iterator helpers define transformations but execute nothing until consumption:

// Eager (arrays): all operations run NOW
const eagerResult = [1, 2, 3, 4, 5]
  .map(x => { console.log('map', x); return x * 2; })
  .filter(x => { console.log('filter', x); return x > 4; });
// Logs: map 1, map 2, map 3, map 4, map 5,
//       filter 2, filter 4, filter 6, filter 8, filter 10

// Lazy (iterators): nothing runs yet
const lazyIter = [1, 2, 3, 4, 5]
  .values()
  .map(x => { console.log('map', x); return x * 2; })
  .filter(x => { console.log('filter', x); return x > 4; });
// Logs: (nothing)

// Consumption triggers execution
[...lazyIter];
// Logs interleaved: map 1, filter 2, map 2, filter 4, ...

Notice the interleaved execution — each value flows through the entire pipeline before the next starts. No intermediate arrays are created.

Infinite Sequences

This is where iterator helpers truly shine. You can work with infinite data sources because values are produced on-demand:

function* infiniteCounter() {
  let i = 0;
  while (true) yield i++;
}

// Get first 5 even numbers greater than 10
const result = infiniteCounter()
  .filter(n => n % 2 === 0)
  .filter(n => n > 10)
  .take(5)
  .toArray();

// [12, 14, 16, 18, 20]

Try this with array methods and your program hangs forever — arrays need a known, finite size.

The Single-Consumption Gotcha

Iterators can only be consumed once. This catches everyone:

const iter = [1, 2, 3]
  .values()
  .map(x => x * 2);

console.log([...iter]); // [2, 4, 6]
console.log([...iter]); // [] — exhausted!

// Must recreate for reuse
const getIter = () => [1, 2, 3].values().map(x => x * 2);
console.log([...getIter()]); // [2, 4, 6]
console.log([...getIter()]); // [2, 4, 6]

If you need to iterate multiple times, either wrap in a factory function or call .toArray() and work with the array.

Short-Circuiting Methods

Some methods stop iteration early — this matters for side effects and performance:

function* withLog() {
  for (let i = 0; i < 1000; i++) {
    console.log('yielding', i);
    yield i;
  }
}

// Only processes until condition fails
withLog().every(x => x < 3);
// Logs: yielding 0, yielding 1, yielding 2, yielding 3
// Returns: false (stops at first failure)

withLog().some(x => x === 2);
// Logs: yielding 0, yielding 1, yielding 2
// Returns: true (stops at first success)

withLog().find(x => x > 5);
// Logs: yielding 0 through yielding 6
// Returns: 6

.take(n) also short-circuits, consuming only n values from the source.

flatMap Edge Cases

.flatMap() flattens one level and works with iterables, not just arrays:

// Flattens iterable results
['hello', 'world']
  .values()
  .flatMap(s => s) // strings are iterable
  .toArray();
// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']

// Only one level
[[1, [2, 3]], [4, [5]]]
  .values()
  .flatMap(x => x)
  .toArray();
// [1, [2, 3], 4, [5]] — nested arrays stay nested

Practical Use Cases

Paginated API Processing:

async function* fetchAllPages(baseUrl) {
  let page = 1;
  while (true) {
    const res = await fetch(`${baseUrl}?page=${page}`);
    const data = await res.json();
    if (data.items.length === 0) return;
    yield* data.items;
    page++;
  }
}

// Get first 100 active users across all pages
const users = await Array.fromAsync(
  fetchAllPages('/api/users')
    .filter(u => u.active)
    .take(100)
);

Log Processing:

// Process huge log file line by line
const errors = logLines
  .values()
  .filter(line => line.includes('ERROR'))
  .map(line => JSON.parse(line))
  .take(1000) // Cap at 1000 for analysis
  .toArray();

Performance Considerations

Iterator helpers aren't always faster. For small arrays that fit in memory, eager array methods can be more efficient due to engine optimizations. Use iterators when:

  • Data is large or streamed
  • You're chaining many transformations (avoiding intermediate arrays)
  • You only need a subset of results (.take(), .find())
  • Working with infinite sequences

Avoid for small, in-memory datasets where the overhead of iterator protocol calls outweighs the benefits.

Advertisement

Related Insights

Explore related edge cases and patterns

JavaScript
Expert
JavaScript Temporal API Gotchas: What Breaks When Migrating from Date
8 min
Browser APIs
Surface
Web Workers Structured Clone: What Can't Cross the Boundary
6 min
JavaScript
Deep
structuredClone(): The Deep Copy That Isn't Always Deep
7 min

Advertisement