webdev.complete
🆕 Modern JavaScript
JavaScript
Lesson 39 of 117
20 min

Iterators & Generators

Symbol.iterator, generators, async iterators, iterator helpers.

Anything you can for...ofis "iterable": arrays, strings, Maps, Sets, NodeLists. The thing that ties them together is a protocol you can implement yourself, and a special function type called a generator that makes implementing it trivial. ES2025 also added iterator helpers, which finally bring the array methods to lazy sequences.

The iterator protocol

An object is iterable if it has a Symbol.iterator method that returns an iterator. An iterator has a .next() method that returns { value, done }. Here's the minimal hand version:

js
const range = {
  from: 1,
  to: 3,

  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      },
    };
  },
};

for (const n of range) console.log(n);   // 1, 2, 3
[...range];   // [1, 2, 3]

That's the long way. Generators give you the same thing with a fraction of the code.

Generators: function* and yield

A generator function is declared with function* and uses yield to produce values. Calling it returns a generator (which is both iterator and iterable):

js
function* range(from, to) {
  for (let i = from; i <= to; i++) {
    yield i;
  }
}

for (const n of range(1, 3)) console.log(n);   // 1, 2, 3
[...range(1, 5)];                              // [1, 2, 3, 4, 5]
What yield really does
Each yield pauses the function and hands the value out to the caller. The next .next() call resumes execution right after that yield. It's a coroutine, basically.

Lazy = only compute what's needed

Because generators yield one value at a time, you can describe infinite sequences and only pay for what you consume:

js
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

const it = naturals();
it.next().value;   // 1
it.next().value;   // 2
it.next().value;   // 3

// Don't try [...naturals()] - that would never finish.

Iterator helpers (ES2025)

For years, arrays had .map, .filter, .reduce, and iterators didn't. ES2025 fixed that. You can chain methods directly on an iterator, and they stay lazy:

js
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

// All lazy: nothing runs until .toArray() pulls
const result = naturals()
  .filter(n => n % 2 === 0)
  .map(n => n * n)
  .take(5)
  .toArray();

// [4, 16, 36, 64, 100]

The new helpers include:

  • .map(fn), .filter(fn), .flatMap(fn)
  • .take(n) - first n values, .drop(n) - skip n values
  • .find, .some, .every, .reduce
  • .toArray() to materialize
Iterator helpers are short-circuiting. A .find or .take(5) on an infinite generator only pulls as many values as needed.

Async iterators: for-await-of

Iterators have an async cousin: Symbol.asyncIterator and for await...of. Made for streams of promises, like paged API responses or file reads:

js
async function* pagedUsers() {
  let page = 1;
  while (true) {
    const res = await fetch("/api/users?page=" + page);
    const data = await res.json();
    if (data.items.length === 0) return;
    for (const user of data.items) yield user;
    page++;
  }
}

for await (const user of pagedUsers()) {
  console.log(user.name);   // one at a time, across pages
}

Try it: a lazy range

// A simple range generator
function* range(from, to, step = 1) {
  for (let i = from; i <= to; i += step) {
    yield i;
  }
}

// Eagerly into an array
console.log([...range(1, 5)]);     // [1, 2, 3, 4, 5]
console.log([...range(0, 20, 4)]); // [0, 4, 8, 12, 16, 20]

// Lazy: take just what you need from an infinite sequence
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

// ES2025 iterator helpers
const squares = naturals()
  .map(n => n * n)
  .take(10)
  .toArray();
console.log("first 10 squares:", squares);

// First prime over 1000
function isPrime(n) {
  if (n < 2) return false;
  for (let i = 2; i <= Math.sqrt(n); i++) {
    if (n % i === 0) return false;
  }
  return true;
}

const big = naturals().drop(1000).find(isPrime);
console.log("first prime > 1000:", big);

// Generator as protocol: works with destructuring and spread
const [a, b, c, ...rest] = range(10, 14);
console.log(a, b, c, rest);

Quiz

Quiz1 / 3

What does `yield` do inside a generator function?

Recap

  • Iterable + iterator is a protocol: Symbol.iterator returns an object with next().
  • function* + yield is the easy way to implement it.
  • Generators are lazy. They can model infinite sequences without blowing memory.
  • ES2025 iterator helpers (.map, .filter, .take, .toArray) let you compose lazy pipelines.
  • Async iterators + for await...of handle streams of promises (e.g. paged API responses).
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.