Skip to content
elephantoo

Iterators & generators

Lesson 30 of 34 16 min read

The iteration protocol, making objects iterable, function* generators and async iteration.


for...of, the spread operator, destructuring, Array.from, Promise.all, new Map(...) — they all work on any iterable. In this lesson you'll learn the simple protocol behind that, make your own objects iterable, and meet generators: functions that can pause and resume. They're perfect for lazy sequences, paginated APIs and data streams.

The iteration protocol#

Two small interfaces:

  • An iterable is an object with a [Symbol.iterator]() method that returns an iterator.
  • An iterator is an object with a next() method that returns { value, done }.

Let's drive an array's iterator by hand:

JavaScript
const letters = ["a", "b"];
const it = letters[Symbol.iterator]();

console.log(it.next()); // { value: 'a', done: false }
console.log(it.next()); // { value: 'b', done: false }
console.log(it.next()); // { value: undefined, done: true }

for...of does exactly this for you: get an iterator, call next() until done is true.

Built-in iterables include arrays, strings, Maps, Sets, arguments, NodeLists and typed arrays. Plain objects are not iterable:

JavaScript
try {
  for (const x of { a: 1 }) console.log(x);
} catch (err) {
  console.log(err.message); // {(intermediate value)} is not iterable
}
for (const [k, v] of Object.entries({ a: 1 })) console.log(k, v); // a 1

Making your own iterable#

JavaScript
class Range {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end = end;
    this.step = step;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const { end, step } = this;
    return {
      next() {
        if (current <= end) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  }
}

const evens = new Range(0, 10, 2);
console.log([...evens]);             // [ 0, 2, 4, 6, 8, 10 ]
for (const n of new Range(1, 3)) console.log(n); // 1, 2, 3
const [first, second] = evens;
console.log(first, second);          // 0 2
console.log(Math.max(...evens));     // 10

Because Range follows the protocol, every language feature that consumes iterables just works.

Generators: iterators made easy#

Writing next() by hand is fiddly. A generator function (function*) creates an iterator for you. Each yield produces a value and pauses the function until the next next() call:

JavaScript
function* countToThree() {
  console.log("starting");
  yield 1;
  console.log("after 1");
  yield 2;
  yield 3;
  console.log("finished");
}

const gen = countToThree(); // nothing runs yet!
console.log(gen.next());    // starting → { value: 1, done: false }
console.log(gen.next());    // after 1 → { value: 2, done: false }
console.log(gen.next());    // { value: 3, done: false }
console.log(gen.next());    // finished → { value: undefined, done: true }

Generators are iterable, so the Range class becomes three lines:

JavaScript
class Range {
  constructor(start, end, step = 1) {
    Object.assign(this, { start, end, step });
  }
  *[Symbol.iterator]() {
    for (let n = this.start; n <= this.end; n += this.step) yield n;
  }
}

console.log([...new Range(5, 15, 5)]); // [ 5, 10, 15 ]

Lazy and infinite sequences#

Generators compute values on demand, so they can describe infinite sequences without running forever:

JavaScript
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

function* take(iterable, n) {
  if (n <= 0) return;
  for (const value of iterable) {
    yield value;
    if (--n === 0) return;
  }
}

console.log([...take(fibonacci(), 10)]); // [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ]

The for...of inside take stops pulling values after 10, so fibonacci never needs to finish.

Iterator helpers (ES2025)

Modern engines (Node 22+, current browsers) add map, filter, take, drop, flatMap, reduce, toArray, some, every, find and forEach directly to iterators — lazily:

JavaScript
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const result = fibonacci()
  .filter((n) => n % 2 === 0)
  .map((n) => n * 10)
  .take(5)
  .toArray();
console.log(result); // [ 0, 20, 80, 340, 1440 ]

// Iterator.from wraps any iterable so you can use the helpers
console.log(Iterator.from(new Set([1, 2, 3])).map((n) => n * n).toArray()); // [ 1, 4, 9 ]

Unlike array methods, no intermediate arrays are created — each value flows through the whole pipeline one at a time.

Delegating with yield*#

yield* yields every value from another iterable — handy for recursion:

JavaScript
const tree = {
  name: "src",
  children: [
    { name: "main.js" },
    { name: "utils", children: [{ name: "format.js" }, { name: "storage.js" }] },
  ],
};

function* walk(node, path = "") {
  const full = path ? `${path}/${node.name}` : node.name;
  yield full;
  for (const child of node.children ?? []) {
    yield* walk(child, full);
  }
}

for (const p of walk(tree)) console.log(p);
Output
src
src/main.js
src/utils
src/utils/format.js
src/utils/storage.js

Two-way communication (briefly)#

next(value) sends a value into the generator — it becomes the result of the paused yield expression. return() finishes the generator early and runs any finally blocks:

JavaScript
function* conversation() {
  try {
    const name = yield "What's your name?";
    const lang = yield `Hi ${name}! Favourite language?`;
    yield `${lang} is a great choice.`;
  } finally {
    console.log("(conversation closed)");
  }
}

const chat = conversation();
console.log(chat.next().value);             // What's your name?
console.log(chat.next("Ada").value);        // Hi Ada! Favourite language?
console.log(chat.next("JavaScript").value); // JavaScript is a great choice.
chat.return();                              // (conversation closed)

for...of calls return() automatically when you break out early, so cleanup code in finally always runs.

Async iteration#

For data that arrives over time — paginated APIs, file streams, websockets — use async generators (async function*) and consume them with for await...of:

JavaScript
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// Pretend API that returns pages of results
async function fetchPage(page) {
  await sleep(50);
  const data = { 1: ["ada", "alan"], 2: ["grace", "linus"], 3: ["margaret"] };
  return { items: data[page], next: page < 3 ? page + 1 : null };
}

async function* allUsers() {
  let page = 1;
  while (page) {
    const { items, next } = await fetchPage(page);
    yield* items;
    page = next;
  }
}

for await (const user of allUsers()) {
  console.log(user);
  if (user === "grace") break; // stops fetching further pages
}
Output
ada
alan
grace

The caller doesn't know or care about pagination; it just loops. Breaking early means page 3 is never requested.

Real APIs work the same way. With fetch, a GitHub-style paginated endpoint becomes:

JavaScript
async function* paginate(url) {
  while (url) {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    yield* await res.json();
    url = res.headers.get("link")?.match(/<([^>]+)>;\s*rel="next"/)?.[1]; // next page URL, if any
  }
}

Node streams are async iterables too:

JavaScript
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";

const lines = createInterface({ input: createReadStream(import.meta.filename) });
let count = 0;
for await (const line of lines) count++;
console.log(`${count} lines`);

And Array.fromAsync(asyncIterable) collects everything into an array.

When to use what#

NeedTool
Loop over existing dataarrays + array methods
A custom collection usable with for...of*[Symbol.iterator]()
Lazy or infinite sequencesgenerator + iterator helpers
Walk a tree / recursive structuregenerator with yield*
Data arriving over time (pages, streams)async generator + for await

Common mistakes#

  • Expecting a generator to run when called — nothing happens until next() (or for...of).
  • Spreading an infinite generator: [...fibonacci()] never finishes. Limit it with take.
  • Reusing a finished generator — each generator object can be iterated only once. Call the function again for a fresh one.
  • Using for...of on an async iterable (use for await...of) or for...in on any iterable.

What's next#

Next, sharpen a skill every developer needs daily: debugging JavaScript with the console, breakpoints and DevTools.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What makes an object usable in for...of?

  2. 2.What does calling a generator function function* gen() {...} return?

  3. 3.Given function* g() { yield 1; yield 2; }, what is [...g()]?

Finished reading?

Mark this lesson complete to track your progress.