Skip to content
{JS World}

Generators & Iterators

Iterators

An iterator is any object that implements the iterator protocol: a next() method that returns { value, done }. Objects that implement the iterable protocol (i.e. have a Symbol.iterator method returning an iterator) can be used with for...of, spread, and destructuring.

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

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

Arrays, strings, Maps, and Sets are all built-in iterables — that’s what powers for...of.

for (const char of "abc") {
  console.log(char); // "a", "b", "c"
}

Generators

A generator is a special function (function*) that can pause and resume its own execution. Calling a generator function doesn’t run its body immediately — it returns a generator object, which is both an iterator and an iterable. Each call to .next() runs the function until the next yield.

function* countUpTo(max) {
  let count = 1;
  while (count <= max) {
    yield count; // pauses here, returns { value: count, done: false }
    count++;
  }
}

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

// Being iterable, generators work directly with for...of
for (const n of countUpTo(3)) {
  console.log(n); // 1, 2, 3
}

Passing values into a generator

.next(value) resumes the generator and becomes the result of the yield expression that was paused.

function* conversation() {
  const name = yield "What's your name?";
  const age = yield `Hi ${name}, how old are you?`;
  return `${name} is ${age} years old.`;
}

const convo = conversation();
console.log(convo.next()); // { value: "What's your name?", done: false }
console.log(convo.next("Ada")); // { value: "Hi Ada, how old are you?", done: false }
console.log(convo.next(36)); // { value: "Ada is 36 years old.", done: true }

yield* — delegating to another generator

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

function* outer() {
  yield 0;
  yield* inner(); // delegates iteration to `inner`
  yield 3;
}

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

Why generators are useful

They make it easy to build custom iterables and lazy sequences — values are only computed when requested, which is handy for infinite sequences.

function* naturalNumbers() {
  let n = 1;
  while (true) {
    yield n++;
  }
}

const numbers = naturalNumbers();
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
console.log(numbers.next().value); // 3
// The generator never runs forever in one go — it only computes
// the next value each time `.next()` is called.

Async generators & for await...of

An async function* combines generators with promises — each yield can be awaited, and the caller consumes it with for await...of. Useful for streaming data (paginated API results, file reads, websocket messages) one chunk at a time instead of loading everything into memory first.

async function* fetchPages(baseUrl) {
  let page = 1;
  while (true) {
    const res = await fetch(`${baseUrl}?page=${page}`); // see fetch-api
    const data = await res.json();
    if (data.items.length === 0) return; // no more pages, stop iterating
    yield data.items;
    page++;
  }
}

async function processAllPages() {
  for await (const items of fetchPages("/api/users")) {
    console.log("Got a page of", items.length, "items");
  }
}
// A simpler async generator, no network involved
async function* delayedNumbers() {
  for (let i = 1; i <= 3; i++) {
    await new Promise((resolve) => setTimeout(resolve, 100)); // simulate async work
    yield i;
  }
}

async function run() {
  for await (const n of delayedNumbers()) {
    console.log(n); // 1, 2, 3 — each ~100ms apart
  }
}
run();

for await...of also works with regular (non-async) iterables containing promises — it awaits each value before handing it to the loop body.

async function printAll(promises) {
  for await (const value of promises) {
    console.log(value);
  }
}

printAll([Promise.resolve(1), Promise.resolve(2), 3]); // 1, 2, 3 — non-promise values pass through as-is