Iterators, Generators & Iterator Helpers

JavaScript · Lesson 51 of 61 7 min read Updated

You have used for...of with arrays, strings, Maps and Sets. Have you wondered how one loop works with so many different things? They all follow the same small agreement, called the iteration protocol. Once you understand it, you can make your own objects loopable, produce values only when they are needed, and process long or even endless sequences without building giant arrays.

In this lesson you will look inside the iteration protocol, write generator functions with function* and yield, use the ES2025 iterator helpers such as .filter() and .take(), and loop over values that arrive over time with for await...of. You should know for...of and for...in and Map & Set.

The iteration protocol

An object is iterable if it has a method stored under the special key Symbol.iterator. Calling that method returns an iterator: an object with a next() method. Each call to next() returns an object with two properties: value, the next item, and done, which becomes true when there is nothing left.

JavaScriptRun it yourself
const stations = ["Delhi", "Rohtak"];
const iterator = stations[Symbol.iterator]();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
Output
{ value: 'Delhi', done: false }
{ value: 'Rohtak', done: false }
{ value: undefined, done: true }

This is exactly what for...of does behind the scenes: it asks for an iterator, calls next() again and again, and stops when done is true. Spread syntax ([...x]), Array.from(), destructuring and new Set(x) all use the same protocol.

Generator functions

Writing next() methods by hand is fiddly. A generator function does it for you. Write it with function* (note the star) and use yield to hand out each value. Calling a generator function does not run its body. It returns a generator object, which is an iterator. The body only runs, step by step, as values are requested: each next() runs until the next yield and then pauses there.

JavaScriptRun it yourself
function* countdown(from) {
  console.log("(countdown started)");
  for (let n = from; n > 0; n--) {
    yield n;
  }
}

const launch = countdown(3);
console.log("Generator created");
console.log(launch.next());
console.log(launch.next().value, launch.next().value);
console.log(launch.next());

console.log([...countdown(5)]);
Output
Generator created
(countdown started)
{ value: 3, done: false }
2 1
{ value: undefined, done: true }
(countdown started)
[ 5, 4, 3, 2, 1 ]

"(countdown started)" prints only after the first next() call, not when the generator is created. This "do the work only when asked" behaviour is called lazy evaluation.

Making your own objects iterable

The easiest way to make a class work with for...of is a generator method named *[Symbol.iterator](). Here a metro timetable produces departure times on demand:

JavaScriptRun it yourself
class MetroTimetable {
  constructor(first, last, gapMinutes) {
    this.first = first; // minutes after midnight
    this.last = last;
    this.gap = gapMinutes;
  }

  *[Symbol.iterator]() {
    for (let t = this.first; t <= this.last; t += this.gap) {
      const hours = String(Math.floor(t / 60)).padStart(2, "0");
      const minutes = String(t % 60).padStart(2, "0");
      yield `${hours}:${minutes}`;
    }
  }
}

const morning = new MetroTimetable(6 * 60, 7 * 60, 15);

for (const time of morning) {
  console.log(`Train at ${time}`);
}
console.log(Array.from(morning).length);
Output
Train at 06:00
Train at 06:15
Train at 06:30
Train at 06:45
Train at 07:00
5

The timetable stores only three numbers, yet it can be looped over, spread or turned into an array like any built-in collection.

Iterator helpers

Because generators are lazy, they can even describe endless sequences, such as every year from now on. You obviously cannot turn an endless sequence into an array. Before ES2025 you had to write loops with break. Now iterators have their own chainable methods, similar to the array methods you know from Array Methods:

MethodWhat it does
.map(fn), .filter(fn), .flatMap(fn)Transform or select values, lazily
.take(n), .drop(n)Keep only the first n values, or skip them
.toArray()Collect the remaining values into an array
.reduce(), .forEach(), .some(), .every(), .find()Consume the values and give a result

The first three rows return new iterators, so nothing happens until something consumes them. Iterator.from(obj) turns any iterator or iterable into one that has these helpers.

JavaScriptRun it yourself
function* yearsFrom(start) {
  for (let year = start; ; year++) {
    yield year; // never ends: values are made only when asked for
  }
}

const isLeap = (y) => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;

const nextLeapYears = yearsFrom(2026)
  .filter(isLeap)
  .take(5)
  .toArray();
console.log(nextLeapYears);

const prices = new Map([
  ["Samosa", 20],
  ["Paneer tikka", 240],
  ["Masala chai", 30],
  ["Thali", 180],
]);

const pricey = prices
  .entries()
  .filter(([, price]) => price >= 100)
  .map(([dish, price]) => `${dish} (₹${price})`)
  .toArray();
console.log(pricey);

const firstCheap = prices.keys().find((dish) => prices.get(dish) < 25);
console.log(firstCheap);
console.log(prices.values().reduce((sum, p) => sum + p, 0));
Output
[ 2028, 2032, 2036, 2040, 2044 ]
[ 'Paneer tikka (₹240)', 'Thali (₹180)' ]
Samosa
470

yearsFrom(2026) would run forever on its own, but take(5) stops asking after five leap years, so the generator only ever produces the years from 2026 to 2044. The Map example shows another benefit: prices.entries() is already an iterator, so you can filter and map it directly, without first copying everything into an array.

Values that arrive over time

An async generator, written async function*, can await between yields. You loop over it with for await...of, which waits for each value in turn. This suits data that arrives piece by piece, such as pages of results from a server or a live score feed.

JavaScriptRun it yourself
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function* liveOver() {
  const balls = ["1", "4", "dot", "W", "6", "2"];
  for (const ball of balls) {
    await wait(300); // pretend each ball arrives from a server
    yield ball;
  }
}

for await (const ball of liveOver()) {
  console.log(`Ball: ${ball}`);
}
console.log("Over complete");
Output
Ball: 1
Ball: 4
Ball: dot
Ball: W
Ball: 6
Ball: 2
Over complete

Common mistakes

  • Forgetting the star. function countdown() with yield inside is a syntax error. Write function*.
  • Reusing a used-up generator. A generator object can be consumed only once. [...gen] a second time gives []. Call the generator function again for a fresh one.
  • Spreading an endless generator. [...yearsFrom(2026)] never finishes and freezes the page. Use .take() first.
  • Expecting arrays from helpers. .map() on an iterator returns another iterator. Add .toArray() when you need an array.
  • Using yield inside a callback. yield cannot appear inside a normal function nested in the generator, such as a forEach callback. Use a for...of loop instead.

Key points

  • Iterables have a Symbol.iterator method; iterators have next(), which returns { value, done }.
  • for...of, spread, Array.from() and destructuring all use this protocol.
  • function* and yield create lazy generators that run step by step as values are requested.
  • A *[Symbol.iterator]() method makes your own class iterable.
  • ES2025 iterator helpers (map, filter, take, drop, toArray and more) work lazily, even on endless sequences.
  • async function* with for await...of handles values that arrive over time; Iterator.concat() (ES2026) joins iterables.

Exercise

Write a generator tokenNumbers(prefix) for a hospital queue that endlessly yields tokens like "OPD-1", "OPD-2", "OPD-3" and so on. Then use iterator helpers to get the first three tokens whose number is even, as an array. Expected result: [ 'OPD-2', 'OPD-4', 'OPD-6' ].

Show solution
JavaScript
function* tokenNumbers(prefix) {
  for (let n = 1; ; n++) {
    yield `${prefix}-${n}`;
  }
}

const evenTokens = tokenNumbers("OPD")
  .filter((token) => Number(token.split("-")[1]) % 2 === 0)
  .take(3)
  .toArray();

console.log(evenTokens); // [ 'OPD-2', 'OPD-4', 'OPD-6' ]

The generator is endless, but take(3) stops the chain after three matches, so only six tokens are ever created.

Quick Quiz

1. What does calling a generator function such as countdown(3) do?

2. What does an iterator's next() return when the sequence is finished?

3. Why can yearsFrom(2026).filter(isLeap).take(5).toArray() finish even though yearsFrom never ends?

4. How do you loop over values from an async function* generator?

Learn JavaScript with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call