Skip to content
Codeloom
JavaScript

Generators, Iterators, and Symbol.iterator in JavaScript

Master JavaScript generators and iterators -- learn function*, yield, Symbol.iterator, and how to build custom iterable objects.

·6 min read · By Codeloom
Intermediate 11 min read

What you'll learn

  • The iterator protocol and Symbol.iterator
  • Generator functions with function* and yield
  • Building custom iterables for for...of loops

Prerequisites

  • JavaScript functions and objects
  • Understanding of for...of loops

Iterators and generators give you fine-grained control over sequences of values. They power for...of loops, spread syntax, destructuring, and many built-in APIs. Understanding them unlocks patterns for lazy evaluation, infinite sequences, and custom iteration.

The iterator protocol

An iterator is any object with a next() method that returns { value, done }.

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

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

The iterable protocol and Symbol.iterator

An iterable is any object with a [Symbol.iterator]() method that returns an iterator. Arrays, strings, Maps, and Sets are all iterables.

const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        }
        return { done: true };
      },
    };
  },
};

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

console.log([...range]); // [1, 2, 3, 4, 5]
const [a, b] = range;    // a = 1, b = 2

Any object that implements [Symbol.iterator] works with for...of, spread, destructuring, Array.from(), and more.

Generator functions

Generators make creating iterators dramatically simpler. A generator function (declared with function*) automatically returns an iterator. The yield keyword pauses execution and produces a value.

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

const gen = countUp(1, 5);
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }

// Generators are iterable
for (const n of countUp(1, 5)) {
  console.log(n); // 1, 2, 3, 4, 5
}

Compare this to the manual iterator above. The generator version is far more readable.

Infinite sequences

Because generators are lazy (they only compute the next value when asked), you can represent infinite sequences.

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

// Take the first 10 Fibonacci numbers
function take(iterable, count) {
  const result = [];
  for (const value of iterable) {
    result.push(value);
    if (result.length >= count) break;
  }
  return result;
}

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

Passing values into generators

yield is a two-way channel. You can pass values back into the generator via next(value).

function* conversation() {
  const name = yield "What is your name?";
  const age = yield `Hello ${name}! How old are you?`;
  return `${name} is ${age} years old.`;
}

const chat = conversation();
console.log(chat.next());          // { value: "What is your name?", done: false }
console.log(chat.next("Alice"));   // { value: "Hello Alice! How old are you?", done: false }
console.log(chat.next(30));        // { value: "Alice is 30 years old.", done: true }

The value passed to next() becomes the result of the yield expression inside the generator.

yield* for delegation

yield* delegates to another iterable or generator.

function* innerSequence() {
  yield "a";
  yield "b";
}

function* outerSequence() {
  yield 1;
  yield* innerSequence(); // delegates to innerSequence
  yield 2;
}

console.log([...outerSequence()]); // [1, "a", "b", 2]

This is powerful for composing generators together.

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

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

Making a class iterable

You can add [Symbol.iterator] to any class using a generator method.

class Playlist {
  #songs = [];

  add(song) {
    this.#songs.push(song);
    return this;
  }

  *[Symbol.iterator]() {
    for (const song of this.#songs) {
      yield song;
    }
  }
}

const playlist = new Playlist()
  .add("Song A")
  .add("Song B")
  .add("Song C");

for (const song of playlist) {
  console.log(song); // "Song A", "Song B", "Song C"
}

console.log([...playlist]); // ["Song A", "Song B", "Song C"]

Async generators

When combined with async/await, generators can yield promises — producing async iterables consumed with for await...of.

async function* fetchPages(baseUrl, totalPages) {
  for (let page = 1; page <= totalPages; page++) {
    const response = await fetch(`${baseUrl}?page=${page}`);
    const data = await response.json();
    yield data;
  }
}

async function processAllPages() {
  for await (const pageData of fetchPages("/api/users", 5)) {
    console.log(`Got ${pageData.results.length} users`);
  }
}

Async generators are ideal for paginated APIs, streaming data, or any sequence of asynchronous values.

Practical example: ID generator

function* idGenerator(prefix = "id") {
  let counter = 0;
  while (true) {
    yield `${prefix}_${++counter}`;
  }
}

const userId = idGenerator("user");
const orderId = idGenerator("order");

console.log(userId.next().value);  // "user_1"
console.log(userId.next().value);  // "user_2"
console.log(orderId.next().value); // "order_1"

Generator control: return and throw

You can force a generator to finish with return() or inject an error with throw().

function* controlled() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log("Cleanup!");
  }
}

const gen2 = controlled();
console.log(gen2.next());     // { value: 1, done: false }
console.log(gen2.return(99)); // "Cleanup!" -> { value: 99, done: true }
console.log(gen2.next());     // { value: undefined, done: true }

The finally block runs when the generator is returned or thrown into, making it useful for cleanup.

Summary

  • Iterators are objects with next() returning { value, done }.
  • Iterables implement [Symbol.iterator]() and work with for...of, spread, and destructuring.
  • Generators (function* + yield) create iterators with minimal boilerplate.
  • Use yield* to delegate to other generators or iterables.
  • Generators support two-way communication via next(value).
  • Async generators (async function* + yield) produce async iterables for streaming data.

Generators and iterators are one of JavaScript’s most underused features. Once you internalize the lazy evaluation model, you will find them invaluable for processing large datasets, building custom sequences, and composing asynchronous workflows.