Skip to editor content
learningjavascript.orglesson 23 of 25

Generators and Iterators

Most functions run to completion and return a single value. Generators break that rule — they can pause mid-execution, yield a value, and resume exactly where they left off. This makes them ideal for producing sequences lazily, handling infinite data, and building powerful iteration utilities.

Understanding generators starts with understanding the iterator protocol JavaScript uses under the hood.

The Iterator Protocol

An iterator is any object with a next() method that returns { value, done }. When done is true, the sequence is finished.

// A hand-rolled iterator for a range of numbers
function createRange(start, end) {
  let current = start;

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

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

This works, but it is verbose. Generators give you the same behaviour with far less boilerplate.

Generator Functions

A generator function is declared with function*. Inside it, yield pauses execution and sends a value to the caller. Calling the function does not run any code — it returns a generator object that implements the iterator protocol.

function* countdown(from) {
  while (from > 0) {
    yield from--;
  }
  yield "Liftoff!";
}

const launch = countdown(3);
console.log(launch.next()); // { value: 3, done: false }
console.log(launch.next()); // { value: 2, done: false }
console.log(launch.next()); // { value: 1, done: false }
console.log(launch.next()); // { value: 'Liftoff!', done: false }
console.log(launch.next()); // { value: undefined, done: true }

Each call to next() runs the function body until the next yield, then freezes it. Local variables and the position in the function are preserved between calls.

Because generator objects are also iterable (they have a [Symbol.iterator] method that returns this), you can drop them straight into a for...of loop or spread them.

function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

// Take the first 8 Fibonacci numbers from an infinite sequence
const fib = fibonacci();
const first8 = Array.from({ length: 8 }, () => fib.next().value);
console.log(first8); // [0, 1, 1, 2, 3, 5, 8, 13]

// Use for...of with a finite generator
function* range(start, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i;
  }
}

for (const n of range(0, 10, 2)) {
  process.stdout.write(n + " ");
}
// 0 2 4 6 8 10

The infinite fibonacci() generator never throws — it only runs as far as you pull values from it. This is lazy evaluation: work is done on demand, not up front.

Recall

Without scrolling up: back in 16-array-methods, [1, 2, 3, ...].map(...).filter(...) runs to the end and hands you a finished array. The fibonacci() generator above is declared over an infinite while (true) loop, yet consuming only its first 8 values never hangs. What is the core difference between how the array pipeline and the generator produce their values?

Passing Values Back In

next() can accept an argument that becomes the result of the yield expression inside the generator. This turns generators into two-way communication channels.

function* calculator() {
  let result = 0;
  while (true) {
    const input = yield result;
    if (input === null) break;
    result += input;
  }
  return result;
}

const calc = calculator();
calc.next();       // Start the generator (runs to first yield)
console.log(calc.next(10).value); // 10
console.log(calc.next(25).value); // 35
console.log(calc.next(5).value);  // 40
console.log(calc.next(null));     // { value: 40, done: true }

The first next() call with no argument starts execution up to the first yield. After that, each value passed to next() feeds back into the generator.

Predict

This generator logs every value it receives back through next(). Three next() calls each pass an argument. Predict exactly what is logged, in order, before running it.

function* echo() {
const first = yield "ready";
console.log("got:", first);
const second = yield "again";
console.log("got:", second);
}

const g = echo();
g.next("A");
g.next("B");
g.next("C");

Because the generator has not yet reached a yield when you make the very first next() call, there is no paused yield expression to receive a value — so any argument to that first next() is discarded. Only from the second next() onward does the argument land as the result of the yield the generator is currently paused on.

Making Objects Iterable

You can make any object work with for...of by adding a [Symbol.iterator] method. Generators make this concise.

const playlist = {
  tracks: ["Intro", "Main Theme", "Battle", "Credits"],

  *[Symbol.iterator]() {
    for (const track of this.tracks) {
      yield track;
    }
  }
};

for (const track of playlist) {
  console.log("Now playing:", track);
}

// Spread also works
console.log([...playlist]);
// ['Intro', 'Main Theme', 'Battle', 'Credits']

The *[Symbol.iterator]() shorthand defines a generator method directly on the object.

Delegating with yield*

yield* delegates to another iterable, yielding all of its values in sequence. It works with any iterable — arrays, strings, other generators.

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

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

function* combined() {
  yield* letters();
  yield* numbers();
  yield* "cd"; // Strings are iterable character by character
}

console.log([...combined()]); // ['a', 'b', 1, 2, 'c', 'd']

This is useful for flattening or composing sequences without loading everything into memory at once.

Debug

This should print the full list of ids, then shout the first one: 'First id: USER-1'. The first line prints fine, but then it crashes with a TypeError. Predict what goes wrong, then fix it so both lines print.

function* idGenerator() {
let id = 1;
while (id <= 3) {
  yield "user-" + id++;
}
}

const ids = idGenerator();

// First pass: collect every id
const all = [...ids];
console.log("All ids:", all);

// Second pass: reuse the same generator to grab the first id
const [firstId] = [...ids];
console.log("First id: " + firstId.toUpperCase());

Expected output: All ids: [ 'user-1', 'user-2', 'user-3' ] First id: USER-1


Try It Yourself

This is a build task: a small program that reports its own pass/fail. Three iteration utilities are stubbed out with only their signatures — you write the logic. Run it as-is and it fails at the first check; implement each until it prints All checks passed.

Everything you need is above. take is a generator that consumes an iterable with for...of and yields until it has emitted n values, then returns to stop (the Fibonacci example pulled exactly 8 values from an infinite source the same way). cycle is an infinite generator — a while (true) loop that keeps re-yielding its items — safe precisely because take only pulls a bounded amount (the lazy evaluation from the Retrieval block). Ring becomes iterable by defining *[Symbol.iterator](), the shorthand from Making Objects Iterable. The spec for each is in the prompt; the checks below the divider are fixed.

Build

Finish the build. Two generators and one class are stubbed with only their signatures; the checks below them fail until each produces the right values. Spec: take(iterable, n) is a generator that yields the first n values pulled from any iterable and then stops — if the iterable runs out before n, it yields only what was there. cycle(items) is a generator that yields the items in order and repeats them forever (it never finishes on its own; take is what bounds it). Ring is a class whose instances are iterable with for...of and spread — give it a *[Symbol.iterator]() that yields each of its stored values in order. Checks run top to bottom, so the first failure is TODO 1 — implement it first, then work down.

const assert = require('assert');

// TODO 1: take(iterable, n) -> generator of the first n values, then stop
function* take(iterable, n) {
// your code here
}

// TODO 2: cycle(items) -> generator that repeats items forever
function* cycle(items) {
// your code here
}

// TODO 3: Ring -> iterable via *[Symbol.iterator]()
class Ring {
constructor(values) {
	this.values = values;
}
// your code here
}

// --- Build checks: these must all pass. Do not edit below this line. ---
assert.deepStrictEqual(
[...take([10, 20, 30, 40], 2)],
[10, 20],
"take(iterable, 2) should yield only the first two values",
);
assert.deepStrictEqual(
[...take(cycle(["a", "b"]), 5)],
["a", "b", "a", "b", "a"],
"take should safely bound the infinite cycle generator to 5 values",
);
assert.deepStrictEqual(
[...new Ring([1, 2, 3])],
[1, 2, 3],
"a Ring should be iterable with spread via its [Symbol.iterator]",
);

console.log("All checks passed.");
console.log("Take:", [...take([10, 20, 30, 40], 2)]);
console.log("Cycle+take:", [...take(cycle(["a", "b"]), 5)]);
console.log("Ring:", [...new Ring([1, 2, 3])]);

Expected output: All checks passed. Take: [ 10, 20 ] Cycle+take: [ 'a', 'b', 'a', 'b', 'a' ] Ring: [ 1, 2, 3 ]

Once it passes, try two variations and predict each before running:

  1. Cycle past one lap. Call [...take(cycle([1, 2, 3]), 7)] — seven values from a three-item cycle. Predict the array before running. You get [1, 2, 3, 1, 2, 3, 1]: cycle re-yields 1, 2, 3 on a loop, and take stops after the seventh pull, landing partway through the third lap. The infinite generator never runs past what take asks for.
  2. Ask for more than there is. Call [...take([5, 6], 10)] — take 10 from an array of only two. Predict the array before running. You get [5, 6], not an error and not a hang: take's for...of exhausts the finite source after two values, so the generator finishes early having yielded fewer than n.

Arrange the code

Reassemble a program that defines a generator producing 1..n, spreads it into an array, squares each value, sums the squares, and logs the total. The lines are shuffled — each line uses a const declared on the line before it, so exactly one top-to-bottom order runs and logs Sum of squares: 30.

  1. const total = squares.reduce((sum, s) => sum + s, 0);
  2. console.log("Sum of squares: " + total);
  3. const nums = [...range(4)];
  4. const squares = nums.map((x) => x * x);
  5. const range = function* (n) { for (let i = 1; i <= n; i++) yield i; };

Key Takeaways

  • An iterator is an object with a next() method returning { value, done } — the protocol that powers for...of, spread, and destructuring.
  • A generator function (function*) returns a generator object that is both an iterator and an iterable.
  • yield pauses execution and sends a value out; calling next() resumes from that exact point.
  • Generators enable lazy evaluation — values are produced on demand, which makes infinite sequences safe and keeps memory flat no matter how many values you generate. The trade-off is speed: stepping a generator is slower per value than reading from an array.
  • Passing an argument to next(value) injects it back as the result of the yield expression, enabling two-way communication.
  • yield* delegates to another iterable, composing sequences without nesting loops.
  • Add *[Symbol.iterator]() to any object to make it natively iterable with for...of and spread.

Pro Tip: Generators shine when you need to model a sequence that is too large (or infinite) to store in memory, or when producing the next value is expensive. Instead of building an array upfront, yield values on demand. Async iteration (for await...of) builds on a sibling of this protocol, Symbol.asyncIterator. Push-based libraries like RxJS solve a related problem but use their own subscribe contract rather than this one — there the producer pushes values, instead of the consumer pulling them. Once you understand generators, a lot of modern JavaScript architecture clicks into place.

Next Steps

Now that you understand the iteration protocol, the next lesson covers Proxy and Reflect — JavaScript's metaprogramming tools that let you intercept and customize how objects behave at the most fundamental level.

Next lesson

Proxy and Reflect

Learn JavaScript Proxy and Reflect to intercept object operations. Build validation, logging, and reactive data patterns.

25 min