ES2024 & ES2025 Features
intermediateObject.groupBy, Promise.withResolvers, Set methods, iterator helpers, and the other additions worth knowing in interviews.
ES2025 iterator helpers explained — map, filter, take, drop on any iterator, lazy evaluation vs array chaining, generators, and interview implementations.
Until ES2025, map/filter/reduce lived only on Array.prototype. To transform an iterator — a generator, a Map's entries, a stream of ids — you either spread it into an array first ([...iter].map(...), materializing everything) or wrote generator plumbing by hand. Iterator helpers put the familiar methods directly on iterators, evaluated lazily:
function* naturals() {
let n = 1;
while (true) yield n++;
}
const result = naturals() // infinite source — no problem
.filter((n) => n % 3 === 0)
.map((n) => n * n)
.take(4)
.toArray(); // [9, 36, 81, 144]The full set: map, filter, take, drop, flatMap, forEach, reduce, toArray, some, every, find — plus Iterator.from() to wrap any iterable.
Array chaining is eager: each step builds a full intermediate array.
huge.map(f).filter(g).slice(0, 10);
// walks all of huge twice, allocates two intermediate arraysIterator chaining is lazy: each element flows through the whole pipeline one at a time, and take(10) stops the source after ten survivors:
Iterator.from(huge).map(f).filter(g).take(10).toArray();
// touches only as many elements as needed to produce 10 resultsWhen an interviewer asks "how would you process a 10-million-row dataset's first 100 matches," this is the answer — O(matches) work instead of O(n) allocations, constant memory.
const scores = new Map([["ada", 92], ["alan", 77], ["grace", 98]]);
const top = scores.entries()
.filter(([, score]) => score >= 90)
.map(([name]) => name)
.toArray(); // ["ada", "grace"]No more [...map.entries()] just to chain a filter.
Before ES2025, this was a popular generator exercise — and it still is, phrased as "polyfill take and map for iterators":
function* mapIter(iter, fn) {
for (const value of iter) {
yield fn(value);
}
}
function* takeIter(iter, limit) {
if (limit <= 0) return;
let count = 0;
for (const value of iter) {
yield value;
if (++count >= limit) return; // also triggers source cleanup via .return()
}
}
// Composition works exactly like the built-ins:
[...takeIter(mapIter(naturals(), (n) => n * 2), 3)]; // [2, 4, 6]Talking points while you write it:
for...of handles calling next() and, on early exit, the iterator's return() — which is how generators run their finally blocks (resource cleanup).take closes the underlying iterator when the limit is hit.The async counterparts (AsyncIterator.prototype.map and friends) are on their own track. For now, the interview-safe way to transform an async stream is an async generator:
async function* mapAsync(source, fn) {
for await (const item of source) {
yield fn(item);
}
}If the conversation heads toward async streams, cancellation, and backpressure, bridge to AbortController and batching promises.
.map on an iterator not return an array?" — laziness; nothing runs until consumption (toArray, for...of, spread).for await scheduling behaves.Object.groupBy, Promise.withResolvers, Set methods, iterator helpers, and the other additions worth knowing in interviews.
The ES2024 way to create a promise you settle from outside — and how to implement the deferred pattern it replaces.