Iterators and Generators

Understand the iterable/iterator protocol that powers for...of, spread, and destructuring, plus how generator functions and yield make writing custom iterators dramatically simpler.

Every time you use for...of, spread an array, or destructure an iterable, you are relying on a formal protocol JavaScript defines for "things that can be looped over one value at a time." Understanding that protocol — and generator functions, which implement it for you — unlocks custom, memory-efficient sequences.

The iterable protocol

An object is 'iterable' if it implements a method at the special key Symbol.iterator that returns an iterator. Arrays, strings, Maps, and Sets are all built-in iterables — that's precisely why for...of, spread (...), and destructuring all work on them.

Plain objects are NOT iterable by default (there is no built-in Symbol.iterator on Object.prototype), which is why for...of throws a TypeError on a plain object, while for...in (a different, older construct that iterates keys) does work.

The iterator protocol

An 'iterator' is any object with a .next() method that returns an object shaped like { value: any, done: boolean }. Each call to .next() produces the next value in the sequence; when the sequence is exhausted, done becomes true and value is typically undefined.

for...of works by calling [Symbol.iterator]() on the iterable to get an iterator, then repeatedly calling .next() on it until done is true, assigning each value to the loop variable along the way.

Generator functions

Writing an iterator by hand (managing .next() calls, done flags, and internal state manually) is tedious. Generator functions — declared with function* — solve this: calling a generator function does not run its body immediately, it returns a generator object that is both iterable AND an iterator.

Inside a generator function, the yield keyword pauses execution and produces a value each time the generator's .next() is called, resuming exactly where it left off on the following call. This makes writing custom sequences dramatically simpler than the manual Symbol.iterator approach.

Why generators matter: laziness

A key benefit of generators is laziness: values are produced one at a time, on demand, rather than all at once up front. This lets you represent sequences that would be impossible to store in memory as a full array — including infinite sequences — because each value is only computed when it's actually requested.

Generators are also used to implement custom Symbol.iterator methods concisely: instead of manually returning an object with a .next() method, you can write [Symbol.iterator]() { return someGeneratorFunction(); } or even make the generator method itself the Symbol.iterator.

Quick reference

The * in function* can be placed as function *name() or function* name() — both are valid, but pick one style and stay consistent. yield can only be used directly inside a generator function body, not inside a regular nested callback within it. Generators can also receive values back via gen.next(valueSentIn), making yield an expression that evaluates to whatever was passed to the next .next() call.

// manual iterator object
const manualIterator = {
  values: [1, 2, 3],
  index: 0,
  next() {
    if (this.index < this.values.length) {
      return { value: this.values[this.index++], done: false };
    }
    return { value: undefined, done: true };
  }
};

// generator function — does the same job far more simply
function* countTo(n) {
  for (let i = 1; i <= n; i++) {
    yield i;
  }
}

const gen = countTo(3);
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
gen.next(); // { value: undefined, done: true }

// generators are iterable, so for...of works directly
for (const n of countTo(3)) {
  console.log(n);
}

Code Examples

colors() is a generator function; calling it returns a generator object. Because generator objects are iterable, for...of can loop over it directly, receiving one yielded value per iteration. Expected result: red green blue yield "red"; pauses the function and hands "red" out as the current value.

This makes a custom class iterable by manually implementing Symbol.iterator, which returns an object with a next() method — this is exactly the protocol for...of relies on under the hood, without using a generator. Expected result: Keyboard Mouse

[Symbol.iterator]() { ... } is what makes for...of work on a custom class instance.

infiniteFibonacci is a truly infinite generator — its while(true) loop never finishes, but because generators are lazy, nothing breaks: take() pulls only the first 8 values by calling.next() eight times, and the rest of the (infinite) sequence is simply never computed. Expected result: [ 1, 1, 2, 3, 5, 8, 13, 21 ] while (true) { yield curr;... } is safe here only because a generator's body pauses at each yield instead of running to completion immediately.

function* colors() {
  yield "red";
  yield "green";
  yield "blue";
}

for (const color of colors()) {
  console.log(color);
}
class ShoppingCart {
  constructor() {
    this.items = [];
  }
  add(item) {
    this.items.push(item);
  }
  [Symbol.iterator]() {
    let index = 0;
    const items = this.items;
    return {
      next() {
        return index < items.length
          ? { value: items[index++], done: false }
          : { value: undefined, done: true };
      }
    };
  }
}

const cart = new ShoppingCart();
cart.add("Keyboard");
cart.add("Mouse");
for (const item of cart) {
  console.log(item);
}
function* infiniteFibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

function take(iterable, count) {
  const result = [];
  const iterator = iterable[Symbol.iterator]();
  for (let i = 0; i < count; i++) {
    result.push(iterator.next().value);
  }
  return result;
}

console.log(take(infiniteFibonacci(), 8));

How for...of drives an iterator

for...of is just syntax sugar over repeatedly calling .next() until done is true — a generator function is a convenient way to build that iterator without writing this by hand.

for (const v of iterable) { ... }

  1. iterable[Symbol.iterator]()  ---->  returns an iterator object
  2. iterator.next()  ---->  { value: v1, done: false }   v = v1, run loop body
  3. iterator.next()  ---->  { value: v2, done: false }   v = v2, run loop body
  4. iterator.next()  ---->  { value: undefined, done: true }   loop stops

Important Rules

An object is iterable only if it has a [Symbol.iterator] method that returns an iterator ({ next(): { value, done } }).

for...of, spread (...), and array/object destructuring of iterables all rely on the same Symbol.iterator protocol.

A generator function (function*) automatically returns an object that is both iterable and an iterator.

Generators are lazy — code inside them only runs up to the next yield when .next() is actually called, enabling infinite or very large sequences.

Where this shows up

Generators power async iteration patterns, custom data-structure traversal (like walking a tree one node at a time without building a full array first), paginated API fetching, and libraries like Redux-Saga. The iterable protocol itself is the reason built-ins like Array, String, Map, and Set all work seamlessly with for...of and spread.

for (const value of plainObject) { console.log(value); } for (const key in plainObject) { console.log(plainObject[key]); } // or Object.entries(plainObject)

Plain objects do not implement Symbol.iterator by default, so for...of throws a TypeError on them; use for...in, Object.keys/values/entries, or make the object iterable yourself.

function* infinite() { while (true) yield 1; } console.log([...infinite()]);

function* infinite() { while (true) yield 1; } console.log(take(infinite(), 5));

Spreading an infinite generator into an array tries to exhaust it completely, which never happens — it will hang and eventually crash; only pull a bounded number of values from an infinite generator.

[Multiple Choice] What must an object have to be usable in a for...of loop? A. A .length property B. A [Symbol.iterator] method that returns an iterator C. A .next() method directly on the object D. It must be an array

B. A [Symbol.iterator] method that returns an iterator — for...of works on any iterable — any object with a [Symbol.iterator] method that returns an iterator object with .next(). Arrays, strings, Maps, Sets, and generator objects all qualify.

[True / False] Calling a generator function (one declared with function*) immediately executes its entire body. A. True B. False

B. False — False. Calling a generator function only creates and returns a generator object; none of the function body runs until .next() is called, and it only runs up to the next yield each time.

Write a generator function range(start, end) that yields every integer from start up to (but not including) end. Use it with for...of to print the numbers 5 through 9.

function* range(start, end) { for (let i = start; i < end; i++) { yield i; } } for (const n of range(5, 10)) { console.log(n); }

Use a for loop inside the generator with yield i instead of collecting results into an array.