Generators & Iterators

Generator functions, the iterator protocol, and custom iterables

Iterators and Iterables

The iterator protocol defines a standard way to produce a sequence of values. An iterable is any object that can be iterated over (like arrays, strings, Maps). Understanding these protocols enables you to create custom data structures that work with for...of loops.

Key Concepts

  • Iterable — Object with [Symbol.iterator] method
  • Iterator — Object with next() method returning {value, done}
  • Generator — Function that can pause and resume execution
  • yield — Pauses generator and returns a value

The Iterator Protocol

// Manual iterator
const array = [1, 2, 3];
const iterator = array[Symbol.iterator]();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

// for...of uses this protocol internally
for (const value of array) {
  console.log(value); // 1, 2, 3
}

Creating Custom Iterables

// Object implementing the iterable protocol
const range = {
  start: 1,
  end: 5,
  
  // Make it iterable
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    
    // Return an iterator object
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

// Now it works with for...of
for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

// And spread operator
console.log([...range]); // [1, 2, 3, 4, 5]

Generator Functions

Generators are functions that can pause execution and yield multiple values:

// Generator function syntax (note the *)
function* numberGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = numberGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

// Generators are iterable
for (const num of numberGenerator()) {
  console.log(num); // 1, 2, 3
}

console.log([...numberGenerator()]); // [1, 2, 3]

Practical Generator Examples

// Infinite sequence
function* infiniteSequence() {
  let i = 0;
  while (true) {
    yield i++;
  }
}

const sequence = infiniteSequence();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
// ...continues forever

// Range generator (simpler than manual iterator)
function* range(start, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i;
  }
}

console.log([...range(1, 5)]);     // [1, 2, 3, 4, 5]
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8, 10]

// ID generator
function* idGenerator(prefix = "id") {
  let id = 1;
  while (true) {
    yield `${prefix}_${id++}`;
  }
}

const userIds = idGenerator("user");
console.log(userIds.next().value); // "user_1"
console.log(userIds.next().value); // "user_2"

yield* for Delegation

// Delegate to another iterable
function* concat(...iterables) {
  for (const iterable of iterables) {
    yield* iterable;
  }
}

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

// Recursive generator (tree traversal)
function* traverseTree(node) {
  yield node.value;
  
  if (node.children) {
    for (const child of node.children) {
      yield* traverseTree(child);
    }
  }
}

const tree = {
  value: 1,
  children: [
    { value: 2, children: [{ value: 4 }, { value: 5 }] },
    { value: 3, children: [{ value: 6 }] }
  ]
};

console.log([...traverseTree(tree)]); // [1, 2, 4, 5, 3, 6]

Two-Way Communication

// Passing values INTO a generator
function* calculator() {
  const a = yield "Enter first number";
  const b = yield "Enter second number";
  return a + b;
}

const calc = calculator();

console.log(calc.next());      // { value: "Enter first number", done: false }
console.log(calc.next(10));    // { value: "Enter second number", done: false }
console.log(calc.next(5));     // { value: 15, done: true }

// Generator with accumulator
function* runningTotal() {
  let total = 0;
  while (true) {
    const value = yield total;
    total += value;
  }
}

const totals = runningTotal();
console.log(totals.next().value);   // 0
console.log(totals.next(10).value); // 10
console.log(totals.next(5).value);  // 15
console.log(totals.next(3).value);  // 18

Error Handling in Generators

function* errorHandlingGen() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (error) {
    console.log("Caught:", error.message);
    yield "recovered";
  }
}

const gen = errorHandlingGen();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.throw(new Error("Oops!"))); 
// Caught: Oops!
// { value: "recovered", done: false }

// Generator with return()
const gen2 = errorHandlingGen();
gen2.next(); // { value: 1, done: false }
gen2.return("early exit"); // { value: "early exit", done: true }

Async Generators

// Async generator for paginated API
async function* fetchPages(url) {
  let page = 1;
  let hasMore = true;
  
  while (hasMore) {
    const response = await fetch(`${url}?page=${page}`);
    const data = await response.json();
    
    yield data.items;
    
    hasMore = data.hasNextPage;
    page++;
  }
}

// Use with for await...of
async function getAllItems() {
  const allItems = [];
  
  for await (const items of fetchPages("/api/items")) {
    allItems.push(...items);
  }
  
  return allItems;
}

// Async generator for streaming
async function* readLines(stream) {
  const reader = stream.getReader();
  let buffer = "";
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    buffer += new TextDecoder().decode(value);
    const lines = buffer.split("\n");
    buffer = lines.pop(); // Keep incomplete line
    
    for (const line of lines) {
      yield line;
    }
  }
  
  if (buffer) yield buffer;
}

💡 Key Takeaways

  • • Iterables implement [Symbol.iterator] and work with for...of
  • • Generators are the easiest way to create iterators
  • • Use yield to pause and yield* to delegate
  • • Generators can receive values via next(value)
  • • Async generators with for await...of for async sequences
  • • Great for infinite sequences, lazy evaluation, and streaming data