JavaScript Iterators and Generators

Iterators and generators are advanced JavaScript features that provide powerful, memory-efficient ways to work with sequences of data — especially large or potentially infinite ones. Understanding them deepens the knowledge of how JavaScript loops, spread operators, and async sequences truly work.

What is an Iterator?

An iterator is any object that implements a next() method. Each call to next() returns an object with two properties:

  • value — the current value
  • done — a boolean; true when the sequence is exhausted
function makeIterator(items) {
  let index = 0;
  return {
    next() {
      if (index < items.length) {
        return { value: items[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

let iterator = makeIterator(["Apple", "Banana", "Mango"]);

console.log(iterator.next()); // { value: "Apple",  done: false }
console.log(iterator.next()); // { value: "Banana", done: false }
console.log(iterator.next()); // { value: "Mango",  done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Iterable Protocol

An iterable is an object that has a special method [Symbol.iterator]() which returns an iterator. Built-in iterables include Arrays, Strings, Maps, Sets, and NodeLists.

let fruits = ["Lemon", "Orange", "Grape"];

let iter = fruits[Symbol.iterator]();  // Get the iterator

console.log(iter.next()); // { value: "Lemon",  done: false }
console.log(iter.next()); // { value: "Orange", done: false }
console.log(iter.next()); // { value: "Grape",  done: false }
console.log(iter.next()); // { value: undefined, done: true }

Making a Custom Iterable Object

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    let current = this.from;
    let last    = this.to;

    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

for (let num of range) {
  console.log(num);  // 1, 2, 3, 4, 5
}

// Spread operator also works on iterables
console.log([...range]); // [1, 2, 3, 4, 5]

What is a Generator?

A generator is a special type of function that can pause its execution midway and resume later. It is defined using function* (note the asterisk) and uses the yield keyword to produce values one at a time.

Real-life analogy: A generator is like a vending machine. Each time a button is pressed, one item comes out. The machine pauses and waits until the next button press.

function* simpleGenerator() {
  yield "First";
  yield "Second";
  yield "Third";
}

let gen = simpleGenerator();

console.log(gen.next()); // { value: "First",  done: false }
console.log(gen.next()); // { value: "Second", done: false }
console.log(gen.next()); // { value: "Third",  done: false }
console.log(gen.next()); // { value: undefined, done: true }

How yield Works

Each yield pauses the function and returns a value. Execution resumes from exactly the same point when next() is called again.

function* steps() {
  console.log("Step 1 started");
  yield "Step 1 complete";

  console.log("Step 2 started");
  yield "Step 2 complete";

  console.log("Step 3 started");
  yield "Step 3 complete";
}

let process = steps();

let s1 = process.next();
// "Step 1 started"
console.log(s1.value); // Step 1 complete

let s2 = process.next();
// "Step 2 started"
console.log(s2.value); // Step 2 complete

Generators Are Iterable

Generators automatically implement the iterable protocol, so they work directly with for...of loops and the spread operator.

function* countUp(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

for (let num of countUp(1, 5)) {
  console.log(num); // 1, 2, 3, 4, 5
}

console.log([...countUp(10, 14)]); // [10, 11, 12, 13, 14]

Infinite Sequence Generator

Generators are perfect for creating infinite sequences because they are lazy — they only produce values when asked.

function* infiniteCounter(start = 0) {
  while (true) {
    yield start++;
  }
}

let counter = infiniteCounter(1);

console.log(counter.next().value); // 1
console.log(counter.next().value); // 2
console.log(counter.next().value); // 3
// Continues forever — only produces values when asked

yield* — Delegating to Another Generator

yield* delegates the yield sequence to another generator or iterable.

function* part1() {
  yield "A";
  yield "B";
}

function* part2() {
  yield "C";
  yield "D";
}

function* combined() {
  yield* part1();
  yield* part2();
  yield "E";
}

console.log([...combined()]); // ["A", "B", "C", "D", "E"]

Passing Values into a Generator

A value can be passed back into a generator through next(value). The passed value becomes the result of the yield expression inside.

function* calculator() {
  let a = yield "Enter first number:";
  let b = yield "Enter second number:";
  yield `Sum = ${a + b}`;
}

let calc = calculator();
console.log(calc.next().value);    // Enter first number:
console.log(calc.next(10).value);  // Enter second number:
console.log(calc.next(25).value);  // Sum = 35

Generator return() and throw()

return() — End the Generator Early

function* myGen() {
  yield 1;
  yield 2;
  yield 3;
}

let g = myGen();
console.log(g.next());          // { value: 1, done: false }
console.log(g.return("Stop")); // { value: "Stop", done: true }
console.log(g.next());          // { value: undefined, done: true }

throw() — Inject an Error into the Generator

function* safegen() {
  try {
    yield "Running...";
  } catch (e) {
    console.log("Error caught inside generator:", e);
  }
}

let sg = safegen();
sg.next();                     // { value: "Running...", done: false }
sg.throw("Something broke!");  // Error caught inside generator: Something broke!

Generators for Async-like Flow (Concept)

Before async/await, generators were used to write asynchronous code in a synchronous style. Libraries like co used this pattern. Today, async/await replaces this need, but understanding it shows where async/await concepts came from.

// Conceptual example — simplified
function* fetchUserFlow() {
  let user   = yield fetch("https://jsonplaceholder.typicode.com/users/1");
  let orders = yield fetch(`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`);
  return orders;
}
// In practice, use async/await for this pattern today

Iterator vs Generator Comparison

FeatureIteratorGenerator
CreationObject with manual next() methodfunction* with yield
State managementManual (track index)Automatic (pauses at yield)
Infinite sequencesPossible but verboseNatural and easy
ReadabilityMore codeConcise
IterableNeeds Symbol.iterator setupAutomatically iterable

Practical Example — Paginated Data Generator

function* paginate(items, pageSize) {
  for (let i = 0; i < items.length; i += pageSize) {
    yield items.slice(i, i + pageSize);
  }
}

let products = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];
let pages    = paginate(products, 3);

console.log(pages.next().value); // ["A", "B", "C"]
console.log(pages.next().value); // ["D", "E", "F"]
console.log(pages.next().value); // ["G", "H", "I"]
console.log(pages.next().done);  // true

Key Points to Remember

  • An iterator is any object with a next() method that returns { value, done }
  • An iterable implements [Symbol.iterator]() to return an iterator
  • Built-in iterables include arrays, strings, maps, and sets
  • Generators are functions defined with function* that use yield to pause and produce values
  • Calling a generator function returns a generator object — the function does not run immediately
  • Each next() call resumes the generator from where it last paused
  • Generators are automatically iterable — they work with for...of and spread
  • Generators are perfect for lazy evaluation, infinite sequences, and custom iteration logic

Leave a Comment

Your email address will not be published. Required fields are marked *