← All concepts
Beyond the Course

Iterators and Generators

A generator writes a pausable, resumable function that hands out one value at a time.

Definition

An iterator is any object with a `next()` method that returns `{ value, done }`: `value` is the next item, and `done` is true once there is nothing left. An object is iterable if it implements `Symbol.iterator`, a method that returns an iterator, which is what lets it work with `for...of`, spread syntax, and destructuring. Arrays, strings, Maps, and Sets are all built-in iterables. A generator function, written with `function*`, is a shortcut for building an iterator by hand. Calling it does not run the function body; it returns a generator object, which is itself an iterator. Each call to that generator's `next()` runs the body forward until it hits a `yield` expression, at which point execution pauses, the yielded value comes back as `{ value, done: false }`, and every local variable's state is preserved exactly where it left off. The next call to `next()` resumes right after that `yield` and runs until the next one, or until the function returns, which produces `{ value: returnValue, done: true }`. Because a generator only computes the next value when asked, it can represent lazy or even infinite sequences, like a counter that yields forever, without ever running out of memory.

Examples

function* count() {
  let i = 0;
  while (true) yield i++;
}
const it = count();
it.next(); // { value: 0, done: false }
it.next(); // { value: 1, done: false }

Each next() call resumes count() right after its last yield, hands back the current i, and pauses again, with i preserved between calls.

function* upTo(n) {
  for (let i = 1; i <= n; i++) yield i;
}
[...upTo(3)]; // [1, 2, 3]

Spreading a generator drains it by calling next() until done is true, collecting every yielded value into an array.

Common mistakes

  • Assuming calling a generator function runs its body right away; it only returns a generator object, the body runs step by step as next() is called.
  • Forgetting to keep calling next() until done is true, and missing the final value that comes back with a return statement.
  • Treating a generator as reusable after it is drained; once done is true, that generator object is finished and a fresh call to the generator function is needed for a new run.

Key takeaways

  • An iterator is any object with a next() method returning { value, done }.
  • Symbol.iterator is what makes an object work with for...of, spread, and destructuring.
  • function* creates a generator, which pauses at each yield and resumes exactly where it left off, keeping its local state intact.
  • Because generators only compute a value when asked, they can represent lazy or infinite sequences without running out of memory.
Check your understanding

function* upTo(n) { for (let i = 1; i <= n; i++) yield i; } const it = upTo(2); it.next(); it.next(); const last = it.next(); What is last?

Where you see this

  • Building a lazy, possibly infinite sequence, like an ID generator or a stream of Fibonacci numbers, that only computes the next value when asked.
  • Implementing Symbol.iterator on a custom class so instances work directly with for...of and spread syntax.
  • Processing a large collection one item at a time instead of building the whole result in memory up front.
  • Pausing and resuming a step-by-step process, like paging through an API's results one page per call.

Practice this

Further reading

Previous
Regular Expressions
Next
JSON Serialization