🚀AdvancedIterators & Generators

Iterators & Generators

Build custom iteration protocols using Symbol.iterator and generator functions to produce lazy, on-demand sequences.

Overview

JavaScript's iteration protocol is the foundation beneath for-of loops, spread syntax, destructuring, and Array.from. An object is iterable when it has a method at the key Symbol.iterator that returns an iterator an object with a next() method that returns { value, done } pairs. This contract is what lets you iterate over arrays, strings, Maps, Sets, and your own custom data structures with identical syntax. Generator functions (function*) dramatically simplify iterator creation. When you call a generator function it returns a generator object an iterator that also implements the iterable protocol. Inside the generator body, yield suspends execution and hands a value out to the caller. Execution resumes exactly where it left off on the next next() call, preserving all local variables and the position within the function body. This laziness is powerful: generators can represent infinite sequences, model state machines, and orchestrate async flows. Understanding iterators and generators unlocks patterns that cannot be expressed with ordinary functions: co-operative scheduling, pipeline composition, on-demand data generation, and two-way communication channels between caller and generator via next(value). These are the building blocks that async generators and the for-await-of loop are built upon, making them essential knowledge for any advanced JavaScript developer.

Key Points

  • •The iterable protocol requires a Symbol.iterator method that returns an object with a next() method
  • •next() must return { value: any, done: boolean } when done is true the iteration is complete
  • •Generator functions use function* syntax; yield pauses and returns a value to the caller
  • •A generator object is both an iterator and an iterable, making it usable in for-of loops directly
  • •Generators are lazy: no code runs until the first next() call, enabling infinite sequences
  • •return inside a generator sets done: true; throw() propagates an error into the generator body
  • •next(value) passes a value back into the generator the yield expression evaluates to that value
  • •Generator delegation with yield* forwards iteration to another iterable, including other generators

Examples

Custom iterable class and manual iterator protocol

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 range = new Range(1, 10, 2);
console.log([...range]);          // [1, 3, 5, 7, 9]

for (const n of new Range(0, 4)) {
  process.stdout.write(n + " ");  // 0 1 2 3 4
}

Because Range implements Symbol.iterator, spread, for-of, and destructuring all work on it automatically.

Generator function infinite Fibonacci sequence

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

function take(n, iterable) {
  const result = [];
  for (const value of iterable) {
    result.push(value);
    if (result.length === n) break;
  }
  return result;
}

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

// Manual control
const gen = fibonacci();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 1

The generator runs forever but the for-of loop breaks early no infinite loop, because generators are lazy.

Two-way communication with next(value) and generator delegation

function* calculator() {
  let result = 0;
  while (true) {
    const input = yield result;
    if (input === null) break;
    result += input;
  }
  return result;
}

const calc = calculator();
calc.next();          // start the generator (result = 0)
console.log(calc.next(10).value); // 10
console.log(calc.next(5).value);  // 15
console.log(calc.next(3).value);  // 18
console.log(calc.next(null));     // { value: 18, done: true }

// Generator delegation with yield*
function* flatten(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) yield* flatten(item);
    else yield item;
  }
}

console.log([...flatten([1, [2, [3, [4]], 5]])]);
// [1, 2, 3, 4, 5]

The first next() call primes the generator to the first yield. Subsequent calls pass values in and receive the next yielded value.

Common Mistakes

  • Calling a generator function without calling next() and wondering why nothing executes the body only runs when driven by next()
  • Reusing an exhausted generator once done: true is returned, the generator is permanently finished and returns { value: undefined, done: true } forever
  • Forgetting that the first next(value) call cannot pass a value in the argument is discarded because there is no yield expression waiting to receive it yet
  • Confusing yield (pause and emit) with return (end the generator) only return sets done: true

Note

Use generators to model pipelines: compose map, filter, and take as generators that accept iterables, then chain them together. Because each stage is lazy, you process only as many items as needed without building intermediate arrays.

Exercises

Write JavaScript code to solve each exercise. Run it and check your output.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What must an object's Symbol.iterator method return to satisfy the iterator protocol?