JavaScript Iterators & Generators

6 minute read

Every for…of loop, every spread ..., every destructure of an array — they all run on the same hidden contract: the iterator protocol. Generators (function* and yield) are the easy way to author iterators. Together, they unlock lazy sequences, custom iteration, and (with async function*) clean streaming code. This chapter covers all of it.

The Iterator Protocol

An iterator is any object with a next() method that returns { value, done }:

const it = {
  i: 0,
  next() {
    return this.i < 3
      ? { value: this.i++, done: false }
      : { value: undefined, done: true };
  },
};

it.next();   // Output: { value: 0, done: false }
it.next();   // Output: { value: 1, done: false }
it.next();   // Output: { value: 2, done: false }
it.next();   // Output: { value: undefined, done: true }

An iterable is any object with a [Symbol.iterator]() method that returns an iterator. Arrays, strings, Map, Set, arguments, NodeList, and many DOM collections are all iterables.

const list = [10, 20, 30];
const it   = list[Symbol.iterator]();
it.next();    // { value: 10, done: false }

Anywhere for…of Works, So Do These

  • for (const x of iterable)
  • [...iterable]
  • Array.from(iterable)
  • new Set(iterable), new Map(iterable)
  • Destructuring [a, b] = iterable
  • Promise.all(iterable)

Make a class iterable, and all of the above light up for free.

Build Your Own Iterable

class Range {
  constructor(start, end, step = 1) {
    this.start = start; this.end = end; this.step = step;
  }
  [Symbol.iterator]() {
    let n = this.start;
    const { end, step } = this;
    return {
      next: () => n < end
        ? { value: n += 0, done: false, ...(n += step, {}) }   // ugly!
        : { value: undefined, done: true },
    };
  }
}

…and that’s exactly why generators exist.

Generators — function* + yield

A generator function returns an iterator automatically. yield pauses; next() resumes.

function* range(start, end, step = 1) {
  for (let n = start; n < end; n += step) yield n;
}

for (const n of range(1, 6)) console.log(n);
// 1 2 3 4 5

[...range(0, 4)];                     // Output: [0, 1, 2, 3]

That’s it — yield does all the iterator bookkeeping for you.

Make a Class Iterable with *[Symbol.iterator]

class Stack {
  #items = [];
  push(x) { this.#items.push(x); }

  *[Symbol.iterator]() {                       // generator method
    for (let i = this.#items.length - 1; i >= 0; i--) yield this.#items[i];
  }
}

const s = new Stack();
s.push(1); s.push(2); s.push(3);
[...s];                                        // Output: [3, 2, 1]

yield* — Delegate to Another Iterable

function* a() { yield 1; yield 2; }
function* b() {
  yield 0;
  yield* a();                                  // delegate
  yield 3;
}

[...b()];                                      // Output: [0, 1, 2, 3]

yield* works with any iterable — arrays, sets, other generators, your custom classes.

Lazy Sequences (Including Infinite Ones)

Generators only produce values on demand — perfect for infinite or huge sequences:

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

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

[...take(naturals(), 5)];                      // Output: [1, 2, 3, 4, 5]

Nothing is stored — values stream through.

A Tiny Pipeline

function* map(iter, fn)     { for (const v of iter) yield fn(v); }
function* filter(iter, pred){ for (const v of iter) if (pred(v)) yield v; }

const firstFiveSquaredEvens = take(
  map(filter(naturals(), n => n % 2 === 0), n => n * n),
  5
);
[...firstFiveSquaredEvens];                    // Output: [4, 16, 36, 64, 100]

Pure laziness — naturals() is never iterated past element 10.

Passing Values Back In — next(value)

Generators are two-way: yield evaluates to whatever the caller passes into next().

function* echo() {
  while (true) {
    const x = yield;
    console.log("got", x);
  }
}

const g = echo();
g.next();                  // start it — runs until first yield
g.next("hi");              // Output: got hi
g.next("world");           // Output: got world

This is the foundation of coroutines and was the trick that early async/await libraries used to make sync-looking async code.

Throwing Into and Returning Out

function* g() {
  try {
    yield 1;
    yield 2;
  } catch (e) {
    console.log("caught", e.message);
  }
}

const it = g();
it.next();                 // { value: 1 }
it.throw(new Error("x")); // logs "caught x", returns { done: true }

it.return(value) early-terminates and runs finally blocks:

function* g() {
  try { yield 1; yield 2; } finally { console.log("cleanup"); }
}
const it = g();
it.next();                 // 1
it.return(99);             // logs "cleanup"; returns { value: 99, done: true }

for…of automatically calls return() when you break early — generators clean up reliably.

Async Iterators — for await…of

Symbol.asyncIterator + async function* describe streams of promises:

async function* lines(stream) {
  let buffer = "";
  const decoder = new TextDecoder();
  for await (const chunk of stream) {
    buffer += decoder.decode(chunk, { stream: true });
    let i;
    while ((i = buffer.indexOf("\n")) >= 0) {
      yield buffer.slice(0, i);
      buffer = buffer.slice(i + 1);
    }
  }
  if (buffer) yield buffer;
}

const res = await fetch("/big.log");
for await (const line of lines(res.body)) {
  console.log(line);
}

Async generators are perfect for paginated APIs, streaming downloads, server-sent events, and Node streams.

async function* pages(api) {
  let url = api;
  while (url) {
    const { results, next } = await fetch(url).then(r => r.json());
    yield* results;
    url = next;
  }
}

for await (const item of pages("/api/items")) handle(item);

Iterator Helpers (ES2025)

Modern engines add helper methods directly on iterators — so you can chain without your own map/filter/take:

const first5SquaredEvens = naturals()
  .filter((n) => n % 2 === 0)
  .map((n) => n * n)
  .take(5)
  .toArray();
// Output: [4, 16, 36, 64, 100]

Available helpers (when supported): map, filter, take, drop, flatMap, reduce, forEach, some, every, find, toArray. They keep things lazy until a terminal operation (like toArray or forEach).

Tip: Check support in your target runtime — these helpers are new. Where unavailable, the manual map/filter/take generators above are a few lines each.

Real-World Patterns

Use case Tool
Walk a tree (DOM, AST) recursive generator with yield*
Pagination async generator that fetches next page
Throttled / batched processing generator + scheduler
Lazy parsing of a stream async generator yielding tokens
Custom collections (Stack, Graph, LinkedList) *[Symbol.iterator]
Combining sequences yield* delegation

Tree Walk Example

function* walk(node) {
  yield node;
  for (const c of node.children) yield* walk(c);
}

for (const n of walk(document.body)) {
  if (n.tagName === "A") console.log(n.href);
}

Common Mistakes

// ❌ Re-iterating a "used" iterator
const it = [1, 2, 3][Symbol.iterator]();
[...it];                                       // [1, 2, 3]
[...it];                                       // []  — already exhausted

// ✅ Iterate the iterable, not the iterator
const arr = [1, 2, 3];
[...arr]; [...arr];

// ❌ Forgetting * on the method
class A { [Symbol.iterator]() { yield 1; } }   // SyntaxError

// ❌ Mixing `next` value semantics
function* g() { const x = yield; yield x + 1; }
const it = g();
it.next(10);                                   // { value: undefined } — value to FIRST next is ignored
it.next(10);                                   // { value: 11 }

// ❌ Treating an async generator like a sync one
const ag = (async function*() { yield 1; })();
for (const v of ag) {}                         // TypeError — need for await

Best Practices Checklist

  • ✅ Use generators to author custom iteration — almost always simpler than manual {next} objects
  • ✅ Use *[Symbol.iterator]() to make your classes work with for…of and spread
  • ✅ Use yield* to compose generators (and any iterables)
  • ✅ Use async generators + for await…of for paginated APIs and streams
  • ✅ Use iterator helpers (ES2025) where available — they’re lazy by design
  • ✅ Let for…of clean up — it calls return() on early break
  • ❌ Don’t reuse an exhausted iterator
  • ❌ Don’t return huge arrays from a function when a generator can stream them
  • ❌ Don’t put expensive work in the generator body unless it must run per-pull

What’s Next?

You now know the full mainstream JavaScript surface — values, control flow, functions, classes, modules, async, regex, collections, iteration. The final chapter pulls everything together: JavaScript best practices — the conventions, patterns, and trade-offs that turn working code into good code.

← Previous: JS Sets & Maps Next: JS Best Practices →

Updated: