Module: JavaScript
JavaScript·061·4 MIN READ

061: Iterators, Iterables, and Generators

TOPICS COVERED: Iterators, Iterables, and Generators

Outcomes

By the end of this lesson, you can:

  • explain the iterable and iterator protocols;
  • identify values that work with for...of;
  • use Symbol.iterator;
  • create custom iterables;
  • write generator functions with function*;
  • use yield to produce lazy sequences;
  • understand where generators improve clarity and where they add unnecessary complexity.

Iterable versus Iterator

An iterable provides a way to create an iterator. An iterator produces a sequence of { value, done } results.

Arrays, strings, Maps, and Sets are iterable.

js
for (const character of "JS") {
  console.log(character);
}

Inspecting the Iterator

js
const values = [10, 20];
const iterator = values[Symbol.iterator]();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

Typical result:

js
{ value: 10, done: false }
{ value: 20, done: false }
{ value: undefined, done: true }

Custom Iterable

js
const range = {
  start: 1,
  end: 3,

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;

    return {
      next() {
        if (current <= end) {
          return {
            value: current++,
            done: false,
          };
        }

        return {
          value: undefined,
          done: true,
        };
      },
    };
  },
};

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

This is valid but verbose.

Generator Functions

Generators make iterator creation much easier.

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

console.log([...range(1, 3)]);

Calling a generator does not immediately run the entire body. It returns a generator object.

js
const sequence = range(1, 3);

console.log(sequence.next());
console.log(sequence.next());

Lazy Production

Generators are useful when values can be produced on demand.

js
function* ids(prefix = "ORD") {
  let number = 1;

  while (true) {
    yield `${prefix}-${number}`;
    number += 1;
  }
}

const orderIds = ids();

console.log(orderIds.next().value);
console.log(orderIds.next().value);
console.log(orderIds.next().value);

The infinite loop is safe only because execution pauses at each yield and the consumer controls how far it advances.

yield*

Delegate to another iterable:

js
function* combined() {
  yield* [1, 2];
  yield* [3, 4];
}

console.log([...combined()]);

Passing Values Back into a Generator

Generators are two-way at a low level:

js
function* conversation() {
  const name = yield "What is your name?";
  yield `Hello ${name}`;
}

const flow = conversation();

console.log(flow.next().value);
console.log(flow.next("Maya").value);

This feature exists, but ordinary application workflows are often clearer with normal functions or async functions.

Iterables and Spread

Spread consumes iterables.

js
const letters = [..."JavaScript"];

So do many constructors:

js
const unique = new Set(["a", "b", "a"]);
const copied = [...unique];

Understanding iterables explains why these language features compose naturally.

Async Iteration Preview

An async iterable can produce values over time.

js
async function* pages(loadPage) {
  let page = 1;

  while (true) {
    const result = await loadPage(page);

    if (result.items.length === 0) {
      return;
    }

    yield result.items;
    page += 1;
  }
}

Consumption:

js
for await (const items of pages(loadPage)) {
  console.log(items);
}

Treat this as an advanced bridge to asynchronous streams rather than a required pattern for every API call.

Worked Example: Paginated Batch Generator

js
function* batches(items, size) {
  if (!Number.isInteger(size) || size <= 0) {
    throw new Error("size must be a positive integer");
  }

  for (let index = 0; index < items.length; index += size) {
    yield items.slice(index, index + size);
  }
}

for (const batch of batches([1, 2, 3, 4, 5], 2)) {
  console.log(batch);
}

Output:

text
[1, 2]
[3, 4]
[5]

Advanced Notes: Iterator Cleanup and Async Iterables

A for...of loop can request cleanup from an iterator when iteration ends early. Generators support this through their return() behavior and finally.

js
function* values() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log("cleanup");
  }
}

for (const value of values()) {
  console.log(value);

  if (value === 2) {
    break;
  }
}

This matters when an iterator owns a resource or lifecycle.

Generator delegation

js
function* menu() {
  yield "Home";
  yield* ["Orders", "Reports"];
  yield "Settings";
}

yield* delegates to any iterable, not just another generator.

Async iterable example

A paginated API can expose data one page at a time:

js
async function* fetchPages(fetchPage) {
  let page = 1;

  while (true) {
    const result = await fetchPage(page);

    if (result.items.length === 0) {
      return;
    }

    yield result.items;
    page += 1;
  }
}

Consumer:

js
for await (const items of fetchPages(fetchPage)) {
  render(items);
}

This can model streams naturally, but ordinary one-shot requests should stay ordinary. Advanced syntax is useful when it clarifies the domain, not when it merely demonstrates language knowledge.

Mistakes and Debugging

  • expecting a generator call to execute the whole body immediately;
  • forgetting that an iterator can be exhausted;
  • reusing an exhausted generator instead of creating a new one;
  • using generators when map, filter, or a simple loop is clearer;
  • converting a huge lazy sequence to an array and losing the memory benefit.

Best Practices

  • Prefer ordinary arrays and loops for ordinary collections.
  • Use generators when lazy sequencing improves the problem model.
  • Keep generator state simple.
  • Use for...of instead of manual next() calls for normal consumption.
  • Learn the protocol because many JavaScript features build on it.

Exercises

Core

Manually call .next() on an array iterator.

Practice

Write function* countdown(from).

Professional Extension

Write function* chunks(array, size) and unit-test invalid sizes and the final partial chunk.

Recap

Iterables define how values can be consumed. Iterators perform the step-by-step consumption. Generators provide a concise language feature for building iterators and lazy sequences.