Technology · JavaScript

Advanced

JavaScript Generators

Learn how generator functions pause and resume execution to build lazy sequences and iterables.

TL;DR
  1. 01Declare generators with function* to pause and resume execution.
  2. 02Use yield to emit values one at a time, lazily.
  3. 03Delegate to nested generators or iterables with yield*.

Generator Basics

  • Use function* to declare a generator that can pause and resume execution with yield.
    function* counter() {
      yield 1;
      yield 2;
    }
    const it = counter();
    it.next(); // { value: 1, done: false }
    
  • Calling a generator function does not run its body — it returns an iterator instead.
    function* greet() {
      console.log("started");
      yield "hi";
    }
    const it = greet(); // nothing logged yet
    it.next(); // logs "started", returns { value: "hi", done: false }
    
  • Each call to .next() runs the body until the next yield or the end of the function.
    function* counter() {
      yield 1;
      yield 2;
    }
    const it = counter();
    [...it]; // [1, 2]
    
  • The final .next() call returns done: true with value set to the return value, if any.
    function* range() {
      yield 1;
      return "finished";
    }
    const it = range();
    it.next(); // { value: 1, done: false }
    it.next(); // { value: "finished", done: true }
    
  • Generators implement both the iterable and iterator protocols, so for...of works directly on them.
    function* counter() {
      yield 1;
      yield 2;
    }
    for (const n of counter()) console.log(n); // 1, 2
    

Controlling Generators with next, return, and throw

  • Pass a value to next(value) to inject data back into the generator at the paused yield.
    function* greet() {
      const name = yield "name?";
      yield `Hello, ${name}!`;
    }
    const g = greet();
    g.next();        // { value: "name?", done: false }
    g.next("Ada");   // { value: "Hello, Ada!", done: false }
    
  • Call .return(value) to stop the generator early, as if a return ran at the current yield.
    function* counter() {
      yield 1;
      yield 2;
    }
    const it = counter();
    it.next();          // { value: 1, done: false }
    it.return("stop");  // { value: "stop", done: true }
    
  • Call .throw(error) to inject an exception at the paused yield, which the generator can catch.
    function* safeGen() {
      try {
        yield 1;
      } catch (e) {
        yield `caught: ${e.message}`;
      }
    }
    const it = safeGen();
    it.next();
    it.throw(new Error("oops")); // { value: "caught: oops", done: false }
    
  • An uncaught throw() closes the generator permanently, so later next() calls just return done: true.
    function* gen() {
      yield 1;
    }
    const it = gen();
    it.next();
    try {
      it.throw(new Error("fatal"));
    } catch (e) {
      console.log(e.message); // "fatal"
    }
    it.next(); // { value: undefined, done: true }
    
  • try/finally blocks inside a generator still run cleanup code when .return() is called.
    function* withCleanup() {
      try {
        yield 1;
        yield 2;
      } finally {
        console.log("cleanup ran");
      }
    }
    const it = withCleanup();
    it.next();
    it.return(); // logs "cleanup ran"
    

Lazy Sequences

  • Generators compute values on demand, so they can represent infinite sequences without infinite memory.
    function* naturals() {
      let n = 1;
      while (true) yield n++;
    }
    const it = naturals();
    it.next().value; // 1
    it.next().value; // 2
    
  • Combine a generator with take-style logic to pull only the values you need.
    function take(iterable, count) {
      const result = [];
      for (const value of iterable) {
        if (result.length >= count) break;
        result.push(value);
      }
      return result;
    }
    take(naturals(), 3); // [1, 2, 3]
    
  • Lazy evaluation avoids wasted work, since nothing computes until something consumes the value.
    function* expensiveValues() {
      yield computeA(); // only runs when first value is requested
      yield computeB();
    }
    
  • Chain generators together to build lazy pipelines, similar to map and filter but evaluated one item at a time.
    function* mapGen(iterable, fn) {
      for (const value of iterable) yield fn(value);
    }
    function* filterGen(iterable, predicate) {
      for (const value of iterable) if (predicate(value)) yield value;
    }
    const evens = filterGen(naturals(), n => n % 2 === 0);
    take(evens, 3); // [2, 4, 6]
    
  • Avoid spreading an infinite generator directly into an array, since that runs forever and crashes the process.
    // Never do this:
    // const all = [...naturals()];
    

Building Custom Iterables

  • Assign a generator to Symbol.iterator to make any object work with for...of and spread syntax.
    class Range {
      constructor(start, end) {
        this.start = start;
        this.end = end;
      }
      *[Symbol.iterator]() {
        for (let i = this.start; i <= this.end; i++) yield i;
      }
    }
    [...new Range(1, 3)]; // [1, 2, 3]
    
  • This removes the need to hand-write a next() method with manual value/done bookkeeping.
    // Manual version requires tracking state yourself:
    // { next() { ... return { value, done }; } }
    // A generator method handles this automatically.
    
  • Generator methods work on plain objects too, not just classes.
    const collection = {
      items: ["a", "b", "c"],
      *[Symbol.iterator]() {
        yield* this.items;
      }
    };
    for (const item of collection) console.log(item);
    
  • Combine multiple sources into one iterable by yielding from each in sequence.
    function* combined(...iterables) {
      for (const iterable of iterables) yield* iterable;
    }
    [...combined([1, 2], [3, 4])]; // [1, 2, 3, 4]
    
  • Custom iterables built with generators work everywhere standard iterables do, including destructuring.
    const [first, second] = new Range(10, 20);
    // first === 10, second === 11
    

Delegating with yield*

  • Use yield* to forward every value from another iterable without writing a manual loop.
    function* inner() {
      yield 1;
      yield 2;
    }
    function* outer() {
      yield* inner();
      yield 3;
    }
    [...outer()]; // [1, 2, 3]
    
  • yield* works on any iterable, not just other generators, including arrays and strings.
    function* flatten() {
      yield* [1, 2];
      yield* "ab";
    }
    [...flatten()]; // [1, 2, "a", "b"]
    
  • yield* also forwards next() values and return()/throw() calls down to the delegated generator.
    function* inner() {
      const x = yield "ask";
      yield `got ${x}`;
    }
    function* outer() {
      yield* inner();
    }
    const it = outer();
    it.next();        // { value: "ask", done: false }
    it.next("value"); // { value: "got value", done: false }
    
  • The expression yield* iterable evaluates to the delegated generator's return value.
    function* inner() {
      yield 1;
      return "done";
    }
    function* outer() {
      const result = yield* inner();
      yield result;
    }
    [...outer()]; // [1, "done"]
    
  • Use recursive yield* calls to flatten nested tree structures into a single sequence.
    function* flattenTree(node) {
      yield node.value;
      for (const child of node.children || []) {
        yield* flattenTree(child);
      }
    }
    
Tips
  1. 01Use generators to model lazy sequences, since values compute only when something calls next() on the iterator.
  2. 02Reach for yield* when delegating to another generator, so you avoid manually looping and re-yielding each value.
  3. 03Wrap a generator in a class method to give custom objects a clean Symbol.iterator implementation for free.
Warnings
  1. 01Calling next() on an exhausted generator just returns done: true forever, so create a fresh instance to iterate again.
  2. 02An uncaught error inside a generator body permanently closes it, so any later next() call just returns done.
  3. 03Storing a generator's full output in an array defeats its lazy nature and can exhaust memory on infinite sequences.
FAQ
JavaScript Event LoopJavaScript Optional Chaining