JavaScript Iterator Helpers: 3 Loops You Can Delete This Week

2026-06-07 · Nico Brandt

You have written this loop. So have I. Probably 50 times.

const result = [];
for (const x of items) {
  if (x.active) result.push(x.name);
}

It works. It is also the kind of code that JavaScript iterator helpers were designed to delete. They shipped in Chrome 122 back in February 2024 and landed in Node.js 22 the same year — they are not vapor and they are not bleeding edge anymore. Every tutorial I have read about them lists the methods and walks you through [1,2,3].values().map(x => x * 2). None of them show you what to actually take out of your codebase. So that is what this is: three patterns you can refactor this week, and the one case where the old loop is still the right call.

What Iterator Helpers Are (in 60 Seconds)

Iterator helpers are methods on Iterator.prototype.map(), .filter(), .take(), .drop(), .flatMap(), .reduce(), .toArray(), .forEach(), .some(), .every(), .find() — that transform iterators lazily, without producing intermediate arrays. The proposal is at Stage 4 (finished) and the API is stable.

Where they run today: Chrome 122+, Node.js 22+, Deno, and Bun. Where they do not run yet: Firefox (tracked at Bugzilla 1568906) and Safari (WebKit 248650). For browser code that needs to cover the gap, core-js/actual/iterator polyfills the whole surface for roughly 3KB gzipped if you import only the helpers you use.

The mental shift is the part worth rereading. Array methods are eager — users.filter(...) allocates a new array on the spot, walks every element, and hands you the result. Iterator helpers are lazy — users.values().filter(...) returns an iterator that has not done a single comparison yet. Nothing runs until you pull on it with .toArray(), .forEach(), a reduce, or a for...of.

That sounds like a parlor trick until you see where the laziness actually matters.

Pattern 1: The filter-then-map That Builds a Throwaway Array

This is the one you write most often, and the one that quietly wastes the most memory.

// Before
const topEmails = users
  .filter(u => u.active)
  .map(u => u.email)
  .slice(0, 10);

Three intermediate arrays. The filter allocates one. The map allocates another. The slice allocates a third. You wanted ten strings.

// After
const topEmails = users.values()
  .filter(u => u.active)
  .map(u => u.email)
  .take(10)
  .toArray();

Zero intermediates. The pipeline pulls one user at a time, runs it through filter and map, and stops as soon as take(10) has its tenth value.

The cost difference is not academic. If users holds 50,000 entries and 30,000 of them are active, the array version allocates a 30,000-element array of email strings — then immediately drops 29,990 of them on the floor. The iterator version visits at most a few dozen users (whatever it takes to find ten actives) and allocates exactly ten strings. CPU win, memory win, and the intent reads better: “give me the first ten active emails,” not “compute everything, then throw most of it away.”

One subtlety worth knowing: .values() is the explicit handle to an array’s iterator. You need it on arrays because Array.prototype.filter is still the eager array method — typing users.filter(...) skips the iterator chain entirely. On Map, Set, NodeList, and URLSearchParams, the .keys(), .values(), and .entries() methods already return iterators, so you can chain helpers on them directly.

Big finite arrays are the easy case. The pattern gets more interesting when the source is not finite at all.

Pattern 2: The Early-Break Loop With a Manual Counter

You have written this one too. A generator that yields rows from a paginated API, and a for...of loop that breaks after twenty results.

// Before
const firstBatch = [];
let count = 0;
for await (const row of paginatedRows()) {
  firstBatch.push(row);
  if (++count >= 20) break;
}

It works. It is also four lines of bookkeeping, one off-by-one waiting to happen, and a manual break that does not document the intent at the call site.

// After
const firstBatch = await paginatedRows().take(20).toArray();

The generator never fetches the page it does not need. If you are building the generator side — wiring up ReadableStream, AbortController, and retry with backoff — the streaming fetch patterns article covers exactly that. .take(20) is the contract on the consumption side: stop after twenty. The generator’s cleanup — closing the underlying connection, aborting the in-flight fetch, releasing the database cursor — runs automatically when iteration ends, because .take() calls the iterator’s return() method on the way out. That last part is the one that quietly fixes real bugs: the manual break version skips return() if you throw in the loop body, which is how you end up with a leaked connection at 2 AM. For non-iterator resources — file handles, database connections — the using keyword solves the same cleanup problem without the boilerplate.

The symmetric tool is .drop(n). If you need everything after the first hundred rows, .drop(100) is one call. The array equivalent is .slice(100), which assumes you have an array to slice. On a generator, .drop() works without materializing anything.

This is the pattern where iterator helpers stop feeling like a refactor and start feeling like the right primitive. Generators were always supposed to compose. Until now, they did not really.

But not every iterable is a generator. Most of the iterables you touch every day are something much more boring — and they get the same treatment.

Pattern 3: The Array.from(map.values()) You No Longer Need

Look at any codebase that uses Map and you will find this:

// Before
const adminIds = Array.from(usersById.values())
  .filter(u => u.role === 'admin')
  .map(u => u.id);

The Array.from is doing nothing useful. usersById.values() already returns an iterator. The only reason for the conversion was to reach .filter and .map. That reason is gone.

// After
const adminIds = usersById.values()
  .filter(u => u.role === 'admin')
  .map(u => u.id)
  .toArray();

Same shape, no materialization. The same fix applies to Set.prototype.values(), NodeList, FormData.entries(), URLSearchParams, even arguments. Anywhere you currently spell Array.from(thing) purely to get at the array methods, you can drop the Array.from and chain directly.

For custom iterables — objects you defined with Symbol.iterator, or someone else’s iterable that you do not control — Iterator.from() is the bridge. It wraps any iterable or iterator and gives you the full helper chain:

const summary = Iterator.from(yourCustomIterable)
  .filter(predicate)
  .take(50)
  .toArray();

That Iterator.from() is the universal escape hatch. Anything iterable, anywhere, gets the helpers.

Three wins so far. Which means it is time for the part most articles skip: the cases where none of this is an upgrade.

When the Old Loop Still Wins

I am not selling you a revolution. Iterator helpers are good. They are not universally good.

Random access. Iterators are one-shot, forward-only. If you need users[42] or items.length or to sort the result, you need an array. Convert to one with .toArray() and stop fighting the tool.

Multiple iterations. An iterator is consumed after one pass. If you want to compute a sum and then iterate again to print, an array is simpler than rebuilding the iterator chain twice. Materialize once.

Small collections. Under fifty items or so, the per-call overhead of the iterator helper objects can actually be slower than the equivalent array methods. The laziness is paying for memory you were not going to allocate anyway. For a list of nav links, just use .filter.

Debugging. console.log(myArray) shows you the whole array. console.log(myIterator) shows you Iterator Helper {} and nothing else. At a breakpoint, call .toArray() to inspect — or expect to spend ten minutes wondering what the pipeline is actually carrying.

The browser support gap. Until Firefox and Safari ship, you either polyfill or stick with arrays on the client. For Node.js 22+ servers, Deno, and Bun, there is no excuse left. Use them.

That is the honest case. One question remains: what is the smallest thing you can do this week to start?

Your Monday-Morning Migration Checklist

Open your editor. Run these greps. The matches are your candidates.

  1. Grep for Array.from(. Nine times out of ten, it is wrapping a Map.values(), a NodeList, or a Set purely to reach the array methods. Each match is a Pattern 3 refactor.

  2. Grep for .filter(...).map( on large collections. Database query results, parsed CSVs, deserialized JSON arrays from upstream services — anywhere the source might be thousands of items. Each match is a Pattern 1 candidate.

  3. Grep for if (++count or let count = 0 inside a for...of. Every match is a Pattern 2 refactor waiting to happen, and probably an off-by-one you have not noticed yet.

  4. Set up polyfills if you ship to browsers. Add core-js/actual/iterator to your bundle entry point. On Node.js 22+ servers, do nothing — you already have the API.

  5. A one-line lint hint. eslint-plugin-unicorn does not yet have a prefer-iterator-helpers rule, but a custom rule that flags Array.from(x.values()).filter( is maybe twenty lines of AST matching. Worth it if your team writes the pattern often.

Pick one of the three patterns. Grep tonight. Ship the diff tomorrow.

The Bottom Line

The loop at the top of this article — const result = []; for (const x of items) { if (x.active) result.push(x.name); } — is Pattern 1 in disguise. You delete it by writing items.values().filter(x => x.active).map(x => x.name).toArray() and moving on with your day. The same goes for the manual counter in your generator loop and the Array.from(map.values()) in your service layer. Three patterns. Probably twenty matches across a mid-sized codebase. A few hours of work, a noticeably smaller memory footprint, and code that reads like what it is doing.

This is not a revolution. It is a slow refactor that pays you back every time someone reads the file. Pick one pattern, grep your repo, and ship the diff. Next time, we will look at the async iterator helpers proposal — same idea, but for the streaming HTTP and database work where the laziness matters even more. Until then, the JavaScript Temporal API is the next standard-library win worth your weekend.