jsguides

Iterator and Generator Patterns

Iterators and generators give JavaScript a standardized way to walk through data. Instead of managing index variables and checking length, you get a clean protocol that for...of, spread, and destructuring all understand. Once you understand how the iterator protocol works, generators become an elegant way to implement it.

The Iterator Protocol

An iterator is any object with a next() method that returns an iteration result object. That object must have two properties: value (the current element) and done (true when there is nothing left to iterate).

const counter = {
  next() {
    return { value: 1, done: false };
  }
};

The second part of the protocol is iterability — an object becomes iterable when it defines a [Symbol.iterator]() method that returns an iterator. JavaScript’s built-in types like Array, String, Map, and Set all implement this. Plain objects do not.

const isIterable = (obj) => Symbol.iterator in obj;

isIterable([1, 2, 3]);   // true
isIterable('hello');    // true
isIterable(new Map());  // true
isIterable({});         // false

Consuming Iterables

Once an object is iterable, JavaScript gives you several ways to consume it.

for...of is the most common. It calls [Symbol.iterator]() once at the start and then calls next() until done is true.

for (const item of ['a', 'b', 'c']) {
  console.log(item); // a, b, c
}

The spread operator works on any iterable:

const chars = [...'abc']; // ['a', 'b', 'c']

Destructuring also understands iterables:

const [first, ...rest] = new Set([1, 2, 3]);
// first = 1, rest = [2, 3]

Array.from converts any iterable to an array, which is useful for iterables that don’t have array methods built in:

const arr = Array.from(new Map([['a', 1], ['b', 2]]));
// [['a', 1], ['b', 2]]

Custom Iterator Implementations

You can make any object iterable by giving it a [Symbol.iterator]() method. This is useful for your own data structures.

Object-based iterator

function createRange(start, end) {
  let current = start;
  return {
    next() {
      if (current < end) {
        return { value: current++, done: false };
      }
      return { value: undefined, done: true };
    }
  };
}

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

Class-based iterator

class Inventory {
  constructor(items) {
    this.items = items;
    this.index = 0;
  }

  [Symbol.iterator]() {
    return {
      next: () => {
        if (this.index < this.items.length) {
          return { value: this.items[this.index++], done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const inv = new Inventory(['sword', 'shield', 'potion']);
for (const item of inv) {
  console.log(item); // sword, shield, potion
}

One thing to watch: when you use a custom iterator with for...of, the loop consumes it completely. If you manually call next() on the same iterator afterward, you start where the loop left off.

To make an iterator reusable, delegate to the built-in iterator of a stored collection instead of managing your own index:

class ReusableInventory {
  constructor(items) {
    this.items = items;
  }

  [Symbol.iterator]() {
    return this.items[Symbol.iterator](); // delegates to Array iterator
  }
}

Generator Functions

Generator functions (function*) give you a simpler way to build iterators. Instead of managing done manually, you yield values and the function pauses after each one.

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

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

Each call to a generator function creates a fresh Generator object with its own state. Calling the same generator function multiple times gives you independent generators.

Generator functions cannot be arrow functions. There is no async* => syntax either.

You can use generator shorthand inside objects and classes:

const obj = {
  *gen() {
    yield 'a';
    yield 'b';
  }
};

class Series {
  *items() {
    yield 1;
    yield 2;
    yield 3;
  }
}

yield and yield*

yield pauses the generator and returns a value to the caller. When the caller invokes next() again, execution resumes after the yield.

function* gen() {
  const a = yield 'first';
  console.log(a); // 'hello'
  yield 'second';
}

const g = gen();
g.next();          // { value: 'first', done: false }
g.next('hello');   // resumes, a='hello', logs 'hello', pauses at next yield
g.next();          // { value: 'second', done: false }
g.next();          // { value: undefined, done: true }

The first next() call has a quirk: its argument is always discarded because there is no prior yield expression to receive it. This catches people off guard.

yield* delegates to another iterable. The outer generator produces every value from the inner one:

function* inner() {
  yield 'a';
  yield 'b';
}

function* outer() {
  yield* inner();
  yield 'c';
}

[...outer()]; // ['a', 'b', 'c']

yield* with arrays is shorthand for yielding each element individually. This makes flattening recursive structures natural:

function* flattenTree(node) {
  if (node === null) return;
  yield node.value;
  yield* flattenTree(node.left);
  yield* flattenTree(node.right);
}

Generator Instance Methods

Generators come with three instance methods that give you fine-grained control.

generator.next(value) resumes execution and sends a value to be assigned as the result of the current yield expression:

function* counter() {
  let n = 0;
  while (true) {
    const increment = yield n++;
    if (increment !== undefined) n += increment;
  }
}

const c = counter();
c.next();    // { value: 0, done: false }
c.next(10);  // { value: 11, done: false } — skipped 1, added 10
c.next();    // { value: 12, done: false }

generator.throw(error) injects an error at the current yield point. If the generator catches it, execution continues to the next yield. If it doesn’t catch, the error propagates to the caller:

function* gen() {
  try {
    yield 'paused';
  } catch (e) {
    console.log('Caught:', e.message); // Caught: boom
  }
  yield 'resumed';
}

const g = gen();
g.next();                      // { value: 'paused', done: false }
g.throw(new Error('boom'));   // resumes, catches, logs, pauses at next yield
g.next();                      // { value: 'resumed', done: false }

generator.return(value) immediately terminates the generator, returning { value, done: true }. Any finally blocks run first:

function* gen() {
  try {
    yield 1;
    yield 2;
  } finally {
    console.log('cleanup');
  }
}

const g = gen();
g.next();           // { value: 1, done: false }
g.return('early'); // logs 'cleanup', returns { value: 'early', done: true }
g.next();           // { value: undefined, done: true }

Async Iterators and Generators

ES2018 added async iterators for handling streams of asynchronous data. An object implements the async iterator protocol when it has a [Symbol.asyncIterator]() method that returns an async iterator — an iterator whose next() returns a Promise.

const asyncRange = {
  [Symbol.asyncIterator]() {
    let step = 0;
    return {
      next() {
        return new Promise((resolve) => {
          setTimeout(() => {
            if (step < 3) {
              resolve({ value: step++, done: false });
            } else {
              resolve({ value: undefined, done: true });
            }
          }, 50);
        });
      }
    };
  }
};

for await (const val of asyncRange) {
  console.log(val); // 0, 1, 2
}

Async generators (async function*) make this cleaner:

async function* fetchPages(baseUrl) {
  let page = 1;
  while (page <= 3) {
    const data = await fetch(`${baseUrl}?page=${page}`).then(r => r.json());
    yield data;
    page++;
  }
}

for await (const pg of fetchPages('/api/data')) {
  console.log(pg);
}

One key difference from regular generators: yield without await inside an async function* returns a Promise-wrapped value. You need for await...of or manual .next() awaiting to consume it.

Practical Patterns

Infinite sequences

Generators are ideal for infinite sequences because they generate values on demand rather than storing them all in memory:

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

const fib = fibonacci();
fib.next().value; // 0
fib.next().value; // 1
fib.next().value; // 1
fib.next().value; // 2

Use .take() to consume a finite portion without hanging:

function take(iterator, count) {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          if (count > 0) {
            count--;
            return iterator.next();
          }
          return { value: undefined, done: true };
        }
      };
    }
  };
}

[...take(fibonacci(), 5)]; // [0, 1, 1, 2, 3]

Pagination helper

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

for (const page of paginate(Array.from({ length: 55 }, (_, i) => i), 20)) {
  console.log(page.length); // 20, 20, 15
}

Tree walking with generators

function* walkTree(node) {
  if (!node) return;
  yield node;
  yield* walkTree(node.left);
  yield* walkTree(node.right);
}

Linked list iterator

class ListNode {
  constructor(value, next = null) {
    this.value = value;
    this.next = next;
  }
}

class LinkedList {
  constructor(head) {
    this.head = head;
  }

  *[Symbol.iterator]() {
    let current = this.head;
    while (current) {
      yield current.value;
      current = current.next;
    }
  }
}

const list = new LinkedList(
  new ListNode(1, new ListNode(2, new ListNode(3)))
);
[...list]; // [1, 2, 3]

Common Pitfalls

Single-use iterators. Iterators and generators are consumed once. After a for...of loop finishes, calling next() again returns { value: undefined, done: true }. If you need multiple passes, spread into an array or call the generator factory each time.

Infinite generators and spread. Spreading an infinite generator hangs indefinitely. Always limit infinite generators with a take() helper or a break condition in a for...of.

First next() argument is ignored. The first call to next() on a generator has no prior yield to receive its argument. This is by design — the first call initializes the generator body up to the first yield.

Async iterator next() must return a Promise. If your async iterator’s next() returns a plain object synchronously, for await...of will fail silently or behave unpredictably. Always wrap the return in new Promise(...).

Return values are discarded by next(). When a generator returns a value (via return or falling through to the end), next() still returns { value: undefined, done: true }. The return value is only visible when you call generator.return(value) from outside.

Summary

Iterators give JavaScript a standardized way to expose sequential data. The iterator protocol requires a next() method returning { value, done }, and objects become iterable by defining [Symbol.iterator]().

Generators simplify iterator implementation using function* and yield. Each call to a generator function returns a fresh iterator with its own state. yield* delegates to other iterables, making recursive traversal of trees and nested structures clean.

Async iterators and async generators (ES2018) handle asynchronous data streams using [Symbol.asyncIterator]() and async function*, consumed with for await...of.

These patterns shine for lazy evaluation, infinite sequences, tree traversal, and building composable data pipelines.

See Also

  • Strategy Pattern — Learn how strategy lets you swap algorithms at runtime
  • State Machines — Model state transitions with explicit states and events
  • Symbol Type — Reference for Symbol.iterator, Symbol.asyncIterator, and other well-known symbols