DocsHub
ES6+

Iterators and Generators

Learn how iterators and generators work in JavaScript and how they power the for...of loop.

Iterators and Generators

You have used for...of to loop over arrays, strings, Maps, and Sets. But have you ever wondered what makes something iterable? Why can you for...of over an array but not a plain object?

The answer is iterators — a protocol that defines how JavaScript steps through values one at a time. And generators are a powerful way to create iterators without all the boilerplate.


The Iterator Protocol

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

  • value — the current value
  • donetrue when there are no more values, false otherwise
const iterator = {
  values: [1, 2, 3],
  index: 0,

  next() {
    if (this.index < this.values.length) {
      return { value: this.values[this.index++], done: false };
    }
    return { value: undefined, done: true };
  }
};

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 }

Every call to next() advances the iterator one step. When done is true — nothing more to iterate.


The Iterable Protocol

An iterable is an object that has a Symbol.iterator method — a special method that returns an iterator.

This is exactly what makes arrays, strings, Maps, and Sets work with for...of. They all implement Symbol.iterator.

const arr = [1, 2, 3];

// Get the iterator from the array
const iterator = arr[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 }

When you use for...of, JavaScript calls Symbol.iterator on the object to get an iterator, then repeatedly calls next() until done is true.


Making a Custom Iterable

You can make any object iterable by adding Symbol.iterator to it.

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 { value: undefined, done: true };
      }
    };
  }
};

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

// Also works with spread
console.log([...range]); // [1, 2, 3, 4, 5]

// And destructuring
const [first, second, ...rest] = range;
console.log(first, second, rest); // 1 2 [3, 4, 5]

Anything that uses iterables under the hood — for...of, spread, destructuring, Array.from() — now works on your custom object.


Generators — The Easy Way to Create Iterators

Writing iterators manually is verbose. Generators are a special kind of function that creates iterators automatically — with much cleaner syntax.

A generator function uses function* (note the asterisk) and yield to produce values one at a time.

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

const gen = simpleGenerator();

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 }

How Generators Work

yes no Call generator function Returns a generator objectno code runs yet Call .next Run until next yield yield found? Pause herereturn value Function endsdone: true

The generator function does not run when called — it returns a generator object. Code only runs when you call .next(). It runs until it hits a yield — pauses there — returns the yielded value — and waits for the next .next() call.

function* steps() {
  console.log("Step 1 running");
  yield "Step 1 done";

  console.log("Step 2 running");
  yield "Step 2 done";

  console.log("Step 3 running");
  yield "Step 3 done";

  console.log("All steps complete");
}

const gen = steps();

console.log(gen.next().value);
// Step 1 running
// Step 1 done

console.log(gen.next().value);
// Step 2 running
// Step 2 done

console.log(gen.next().value);
// Step 3 running
// Step 3 done

console.log(gen.next().value);
// All steps complete
// undefined

The function literally pauses at each yield and resumes from exactly that point on the next .next() call. No other function in JavaScript can do this.


Using Generators With for...of

Generator objects are iterables — you can use them directly with for...of.

function* range(from, to, step = 1) {
  for (let i = from; i <= to; i += step) {
    yield i;
  }
}

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

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

This replaces the verbose custom iterable we wrote earlier with three clean lines.


Infinite Generators

Generators can yield values forever — they only run when you ask for the next value, so they never block.

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

const counter = infiniteCounter(1);

console.log(counter.next().value); // 1
console.log(counter.next().value); // 2
console.log(counter.next().value); // 3
// ... forever, but only when you ask
// Take only what you need
function take(generator, n) {
  const result = [];
  for (const value of generator) {
    result.push(value);
    if (result.length === n) break;
  }
  return result;
}

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

A regular array of infinite numbers is impossible — you would run out of memory. A generator produces values lazily — only when requested.


yield* — Delegating to Another Iterable

yield* delegates to another iterable — yielding all its values in sequence.

function* numbers() {
  yield 1;
  yield 2;
}

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

function* combined() {
  yield* numbers(); // yield all values from numbers
  yield* letters(); // then all from letters
  yield "done";
}

console.log([...combined()]); // [1, 2, "a", "b", "done"]
// Works with any iterable
function* flatten(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      yield* flatten(item); // recursively flatten
    } else {
      yield item;
    }
  }
}

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

Passing Values Into a Generator

next() can accept a value — which becomes the result of the yield expression inside the generator.

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

const calc = calculator();

console.log(calc.next().value);    // "Enter first number:"
console.log(calc.next(10).value);  // "Enter second number:" — 10 is assigned to a
console.log(calc.next(20).value);  // "Result: 30" — 20 is assigned to b

The first .next() starts the generator — the value passed to the first .next() is ignored. Each subsequent .next(value) resumes the generator and that value becomes what yield evaluates to.


Real Use — Paginated API

Generators are perfect for lazy data loading — fetch the next page only when needed.

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

  while (true) {
    const response = await fetch(`${baseUrl}?page=${page}`);

    if (!response.ok) break;

    const data = await response.json();

    if (data.length === 0) break; // no more pages

    yield data; // yield this page's data

    page++;
  }
}

// Usage — process one page at a time
async function loadAllUsers() {
  const pages = fetchPages("https://api.example.com/users");

  for await (const page of pages) {
    for (const user of page) {
      console.log(user.name);
    }
  }
}

async function* creates an async generator — it can await inside and yield data. for await...of consumes it one page at a time. You only fetch the next page when you need it — never loading everything at once.

for await...of works with async iterables — objects that return Promises from next(). Async generators are the cleanest way to create them.


A Real Example — ID Generator

function* createIdGenerator(prefix = "id") {
  let id = 1;
  while (true) {
    yield `${prefix}-${String(id++).padStart(4, "0")}`;
  }
}

const userIds = createIdGenerator("user");
const orderIds = createIdGenerator("order");

console.log(userIds.next().value);  // user-0001
console.log(userIds.next().value);  // user-0002
console.log(userIds.next().value);  // user-0003
console.log(orderIds.next().value); // order-0001
console.log(orderIds.next().value); // order-0002

Each generator maintains its own state independently. userIds and orderIds never interfere with each other. Clean, stateful, infinite ID generation.


Summary

  • An iterator is an object with a next() method that returns { value, done }
  • An iterable is an object with a Symbol.iterator method that returns an iterator
  • for...of, spread, and destructuring all use the iterator protocol under the hood
  • A generator function uses function* and yield to produce values lazily
  • Generators pause at each yield and resume on the next .next() call
  • Generators are iterables — use them with for...of, spread, and destructuring
  • Infinite generators produce values forever without blocking — only run when asked
  • yield* delegates to another iterable — yielding all its values
  • Pass values into a generator through next(value) — becomes the result of yield
  • Async generators combine async function* with yield — consumed with for await...of

On this page