Back to JavaScript and TypeScript

Generators and Iterators

Iterators are objects that produce values one at a time. Generators are functions that make creating iterators easy you write normal-looking code with yield, and JavaScript handles the state machine for you.

Protocol

The Iterator Protocol

JavaScript defines two simple protocols iterator and iterable that power for...of, spread, destructuring, and more. Any object can opt in.

The iterator protocol

Any object with a next() method that returns { value, done } is an iterator. for...of and spread (...) work on anything that implements this protocol.

// Manual iterator
function makeRange(start, end) {
  let current = start
  return {
    next() {
      if (current <= end) {
        return { value: current++, done: false }
      }
      return { value: undefined, done: true }
    }
  }
}

const iter = makeRange(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

An *iterable* is an object with a [Symbol.iterator]() method that returns an iterator. Arrays, strings, Maps, and Sets are all built-in iterables. for...of calls this method automatically.

// Make an object iterable
const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from
    const last = this.to
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true }
      }
    }
  }
}

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

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

Generators

Generator Functions

Generators implement the iterator protocol automatically. The function* syntax and yield let you write iterators as if they were synchronous, sequential code.

Generator functions and yield

A function* returns a generator object which is both an iterator and an iterable. Each yield pauses execution and hands back a value. The next .next() call resumes from that exact point.

function* countdown(n) {
  while (n > 0) {
    yield n
    n--
  }
  return "done"
}

const gen = countdown(3)
console.log(gen.next())  // { value: 3, done: false }
console.log(gen.next())  // { value: 2, done: false }
console.log(gen.next())  // { value: 1, done: false }
console.log(gen.next())  // { value: "done", done: true }

// Works with for...of too (return value is ignored by for...of)
for (const n of countdown(3)) {
  console.log(n)  // 3, 2, 1
}

Infinite lazy sequences

Generators are lazy they produce values on demand. You can express an infinite sequence without blowing up memory, and take only what you need.

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

function take(n, iterable) {
  const result = []
  for (const value of iterable) {
    result.push(value)
    if (result.length === n) break
  }
  return result
}

console.log(take(5, naturals()))   // [1, 2, 3, 4, 5]
console.log(take(10, naturals()))  // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Two-way communication with next(value)

You can pass a value *into* a generator by calling gen.next(value). That value becomes the result of the yield expression inside the generator. This is how generators implement coroutine-style behaviour.

function* accumulator() {
  let total = 0
  while (true) {
    const n = yield total  // yield sends total out, receives n back
    total += n
  }
}

const acc = accumulator()
acc.next()     // start  { value: 0, done: false }
acc.next(10)   // { value: 10, done: false }
acc.next(20)   // { value: 30, done: false }
acc.next(5)    // { value: 35, done: false }

yield* delegating to another iterable

yield* forwards iteration to another iterable (including another generator). This lets you compose generators cleanly without manual looping.

function* letters() { yield "a"; yield "b"; yield "c" }
function* digits() { yield 1; yield 2 }

function* combined() {
  yield* letters()
  yield* digits()
  yield "end"
}

console.log([...combined()])  // ["a", "b", "c", 1, 2, "end"]

Real World

Real-World Use Cases

Generators shine whenever you need to produce a sequence of values on demand especially when fetching, streaming, or generating identifiers.

Paginated API fetching

A generator can hide the pagination loop behind a simple iterable interface. Callers just use for await...of without knowing how many pages there are.

async function* fetchPages(url) {
  let nextUrl = url
  while (nextUrl) {
    const res = await fetch(nextUrl)
    const data = await res.json()
    yield data.items
    nextUrl = data.nextPageUrl  // null when done
  }
}

for await (const items of fetchPages("/api/users")) {
  processItems(items)
}

Unique ID generator

A simple use case: an infinite generator that produces unique IDs. No state needs to live outside the function.

function* idGenerator(prefix = "id") {
  let n = 1
  while (true) {
    yield `${prefix}-${n++}`
  }
}

const ids = idGenerator("user")
console.log(ids.next().value)  // "user-1"
console.log(ids.next().value)  // "user-2"
console.log(ids.next().value)  // "user-3"