AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

06 Collections and iteration / Lesson 4 of 5

Generators — functions that pause, and lazy pipelines

14 min · +10 XP · 2 exercises · 5 questions

Read the text14 min
Exercises0/2
Quiz5 q

Quick answer

What is a generator in JavaScript?

A generator in JavaScript is a function declared with function* that can pause at each yield and resume later with its local variables intact. Calling it runs nothing; it returns a generator object, an iterator whose next() runs the body up to the next yield. Generators produce values lazily, so infinite sequences and on-demand pipelines take a few lines.

A generator is a function that can pause at yield, hand a value out, and later resume exactly where it stopped with all its local variables intact. Call it and nothing runs; you get a generator object that is both an iterator and an iterable. That makes generators the shortest way to implement the iteration protocol — a range, a tree walk, an infinite sequence in three lines — and the natural way to build lazy pipelines: filter, map and take that touch only the items actually consumed. They also run backwards: next(value) sends data into the paused function, which is how coroutines, state machines and (historically) async/await were built. This lesson covers the syntax, the protocol, delegation with yield*, the pipeline toolkit, and where generators earn their keep.

The syntax and the protocol

function* countdown(n) {
  while (n > 0) yield n--;
  return "done";                          // the value of the final { done: true } result
}
const g = countdown(3);                   // nothing has run yet
g.next();     // { value: 3, done: false }
g.next();     // { value: 2, done: false }
g.next();     // { value: 1, done: false }
g.next();     // { value: "done", done: true }
g.next();     // { value: undefined, done: true } — forever after

[...countdown(3)];                        // [3, 2, 1] — return value is not part of the sequence
for (const n of countdown(3)) { }

function* (or *method() in a class/object, or async function*) marks a generator; arrows cannot be generators. Each next() runs the body until the next yield, whose operand becomes value. A return (or falling off the end) sets done: true. Generator objects are single-use, like every iterator.

Infinite sequences and take

function* naturals() { let n = 1; while (true) yield n++; }
function* take(n, it) { for (const x of it) { if (n-- <= 0) return; yield x; } }
[...take(5, naturals())];                 // [1, 2, 3, 4, 5]

Infinite loops are fine inside a generator because the loop only advances when someone calls next(). The consumer decides how much to compute — take stops pulling, and for…of in take calls the source's return() on the way out.

Lazy pipelines

function* map(fn, it) { for (const x of it) yield fn(x); }
function* filter(pred, it) { for (const x of it) if (pred(x)) yield x; }

const pipeline = take(3, map((x) => x * x, filter((x) => x % 2, naturals())));
[...pipeline];                            // [1, 9, 25] — only 5 naturals were ever generated

Each stage pulls from the one below it only when pulled itself. Compared with arr.filter().map().slice(0, 3): no intermediate arrays, works on infinite or streaming sources, and stops at the first three results instead of processing everything. For a small in-memory array, the array methods are simpler and faster; for large, lazy or infinite data, generators win. Node 22's iterator helpers (it.filter().map().take()) are exactly these functions as methods; on Node 16 you write them once.

yield* — delegation

function* flatten(tree) {
  for (const node of tree) {
    if (Array.isArray(node)) yield* flatten(node);      // hand control to the inner generator
    else yield node;
  }
}
[...flatten([1, [2, [3, [4]]], 5])];      // [1, 2, 3, 4, 5]

function* chain(...its) { for (const it of its) yield* it; }     // works on ANY iterable
[...chain("ab", [1, 2], new Set([3]))];   // ["a", "b", 1, 2, 3]

yield* inner yields every value of inner as if written inline, forwards next(value)/return()/throw() into it, and evaluates to inner's return value. Recursive tree traversals become a few lines; without yield* you would loop and re-yield by hand.

Sending values in: next(value)

function* average() {
  let sum = 0, count = 0;
  while (true) {
    const x = yield count ? sum / count : 0;     // yield gives out the current average and receives the next number
    sum += x; count++;
  }
}
const avg = average();
avg.next();        // prime: runs to the first yield, value 0 (the argument to the first next() is ignored)
avg.next(10).value;   // 10
avg.next(20).value;   // 15

The argument to next(v) becomes the value of the yield expression the generator is paused on. The first next() has no paused yield to deliver to, so its argument is dropped — hence the priming call. gen.return(v) finishes the generator early (running finally blocks) and gen.throw(err) raises inside it at the paused yield — the two channels that make generators full coroutines, and the foundation of the pre-2017 async libraries (co, redux-saga).

Generators as class iterables

class Tree {
  constructor(value, children = []) { this.value = value; this.children = children; }
  *[Symbol.iterator]() {                          // depth-first, pre-order
    yield this.value;
    for (const c of this.children) yield* c;
  }
  *breadthFirst() {
    const queue = [this];
    while (queue.length) { const n = queue.shift(); yield n.value; queue.push(...n.children); }
  }
}

A generator method is the idiomatic way to make a class iterable when the sequence is computed rather than stored — and to offer several orders (breadthFirst()) as plain methods.

try/finally and cleanup

Code after a yield may never run if the consumer stops early — put cleanup in finally; for…of's break calls return(), which runs it. A generator holding a file handle or a lock should always be structured try { yield … } finally { release() }.

Async generators, briefly

async function* yields promises' resolved values and is consumed with for await (const x of gen()) — the pattern for paginated APIs, streams and line-by-line file reading. Module 8 covers them with promises; the mechanics are the ones here plus await.

Common mistakes

  • Expecting the body to run at call time; it runs at the first next().
  • Reusing a generator object for a second loop (single-use); call the generator function again.
  • Forgetting the priming next() when sending values in.
  • Spreading an infinite generator.
  • Returning a value and expecting it in [...gen] — return values are not yielded.
  • Arrow functions with * (syntax error) or yield inside a nested normal function (it is not the generator).

Interview angle

  • "What is a generator?" A function that pauses at yield and resumes on next(), producing an iterator lazily.
  • "Implement take(n, iterable)." A generator with a counter that returns when the count runs out.
  • "Why lazy?" Compute only what is consumed; works on infinite and streaming sources; no intermediate arrays.
  • "What does yield* do?" Delegates to another iterable, yielding its values and forwarding next/return/throw.
  • "How do you send a value into a generator?" gen.next(v) — it becomes the value of the paused yield; prime first.

Key takeaways

  • function* + yield: the body runs on next(), pausing at each yield; return ends it; generator objects are single-use iterators and iterables.
  • Infinite sequences are fine; take/map/filter generators make lazy pipelines that compute only what is consumed.
  • yield* delegates (recursion, chaining any iterables) and forwards the protocol.
  • next(v) sends values in (prime first); return/throw finish or raise inside; finally for cleanup.
  • Generator methods are the idiomatic class iterable; async generators pair with for await.

Common questions

What does yield* do in JavaScript?

yield* inner delegates to another iterable: it yields every value of inner as if written inline, forwards next(value), return() and throw() into it, and evaluates to the return value of inner. It makes recursive traversals, such as flattening a nested array, a few lines long.

How do you pass a value into a generator?

Call gen.next(value): the argument becomes the result of the yield expression the generator is paused on. The first next() has no paused yield to receive it, so its argument is dropped — which is why a generator that takes input is primed with one empty next() first.

Why are generators lazy?

A generator computes its next value only when next() is called, so the consumer decides how much work is done. A pipeline of filter, map and take generators can therefore run over infinite or streaming sources without building intermediate arrays, stopping as soon as enough results are taken.

Can an arrow function be a generator?

No — there is no arrow syntax for generators. Use a function* declaration or expression, a *method() in a class or object literal, or async function* for an async generator. A yield inside an ordinary function nested in a generator does not belong to the generator.

Where does a generator's return value go?

It becomes the value of the final { done: true } result. for…of and spread ignore that result, so [...gen()] never contains the return value; only manual next() calls see it, and it is the value of a yield* expression.

Mark as readSign in to keep your place.

JavaScript Generators Explained: yield and Lazy Pipelines

A JavaScript generator pauses at yield and resumes on next(), producing values lazily. Infinite sequences, lazy pipelines, delegation and sending values in.

  • Course: JavaScript study plan
  • Module: Collections and iteration
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: Node 16

What is a generator in JavaScript?

A generator in JavaScript is a function declared with function* that can pause at each yield and resume later with its local variables intact. Calling it runs nothing; it returns a generator object, an iterator whose next() runs the body up to the next yield. Generators produce values lazily, so infinite sequences and on-demand pipelines take a few lines.

Lesson

A generator is a function that can pause at yield, hand a value out, and later resume exactly where it stopped with all its local variables intact. Call it and nothing runs; you get a generator object that is both an iterator and an iterable. That makes generators the shortest way to implement the iteration protocol — a range, a tree walk, an infinite sequence in three lines — and the natural way to build lazy pipelines: filter, map and take that touch only the items actually consumed. They also run backwards: next(value) sends data into the paused function, which is how coroutines, state machines and (historically) async/await were built. This lesson covers the syntax, the protocol, delegation with yield*, the pipeline toolkit, and where generators earn their keep.

The syntax and the protocol

function* countdown(n) {
  while (n > 0) yield n--;
  return "done";                          // the value of the final { done: true } result
}
const g = countdown(3);                   // nothing has run yet
g.next();     // { value: 3, done: false }
g.next();     // { value: 2, done: false }
g.next();     // { value: 1, done: false }
g.next();     // { value: "done", done: true }
g.next();     // { value: undefined, done: true } — forever after

[...countdown(3)];                        // [3, 2, 1] — return value is not part of the sequence
for (const n of countdown(3)) { }

function* (or *method() in a class/object, or async function*) marks a generator; arrows cannot be generators. Each next() runs the body until the next yield, whose operand becomes value. A return (or falling off the end) sets done: true. Generator objects are single-use, like every iterator.

Infinite sequences and take

function* naturals() { let n = 1; while (true) yield n++; }
function* take(n, it) { for (const x of it) { if (n-- <= 0) return; yield x; } }
[...take(5, naturals())];                 // [1, 2, 3, 4, 5]

Infinite loops are fine inside a generator because the loop only advances when someone calls next(). The consumer decides how much to compute — take stops pulling, and for…of in take calls the source's return() on the way out.

Lazy pipelines

function* map(fn, it) { for (const x of it) yield fn(x); }
function* filter(pred, it) { for (const x of it) if (pred(x)) yield x; }

const pipeline = take(3, map((x) => x * x, filter((x) => x % 2, naturals())));
[...pipeline];                            // [1, 9, 25] — only 5 naturals were ever generated

Each stage pulls from the one below it only when pulled itself. Compared with arr.filter().map().slice(0, 3): no intermediate arrays, works on infinite or streaming sources, and stops at the first three results instead of processing everything. For a small in-memory array, the array methods are simpler and faster; for large, lazy or infinite data, generators win. Node 22's iterator helpers (it.filter().map().take()) are exactly these functions as methods; on Node 16 you write them once.

yield* — delegation

function* flatten(tree) {
  for (const node of tree) {
    if (Array.isArray(node)) yield* flatten(node);      // hand control to the inner generator
    else yield node;
  }
}
[...flatten([1, [2, [3, [4]]], 5])];      // [1, 2, 3, 4, 5]

function* chain(...its) { for (const it of its) yield* it; }     // works on ANY iterable
[...chain("ab", [1, 2], new Set([3]))];   // ["a", "b", 1, 2, 3]

yield* inner yields every value of inner as if written inline, forwards next(value)/return()/throw() into it, and evaluates to inner's return value. Recursive tree traversals become a few lines; without yield* you would loop and re-yield by hand.

Sending values in: next(value)

function* average() {
  let sum = 0, count = 0;
  while (true) {
    const x = yield count ? sum / count : 0;     // yield gives out the current average and receives the next number
    sum += x; count++;
  }
}
const avg = average();
avg.next();        // prime: runs to the first yield, value 0 (the argument to the first next() is ignored)
avg.next(10).value;   // 10
avg.next(20).value;   // 15

The argument to next(v) becomes the value of the yield expression the generator is paused on. The first next() has no paused yield to deliver to, so its argument is dropped — hence the priming call. gen.return(v) finishes the generator early (running finally blocks) and gen.throw(err) raises inside it at the paused yield — the two channels that make generators full coroutines, and the foundation of the pre-2017 async libraries (co, redux-saga).

Generators as class iterables

class Tree {
  constructor(value, children = []) { this.value = value; this.children = children; }
  *[Symbol.iterator]() {                          // depth-first, pre-order
    yield this.value;
    for (const c of this.children) yield* c;
  }
  *breadthFirst() {
    const queue = [this];
    while (queue.length) { const n = queue.shift(); yield n.value; queue.push(...n.children); }
  }
}

A generator method is the idiomatic way to make a class iterable when the sequence is computed rather than stored — and to offer several orders (breadthFirst()) as plain methods.

try/finally and cleanup

Code after a yield may never run if the consumer stops early — put cleanup in finally; for…of's break calls return(), which runs it. A generator holding a file handle or a lock should always be structured try { yield … } finally { release() }.

Async generators, briefly

async function* yields promises' resolved values and is consumed with for await (const x of gen()) — the pattern for paginated APIs, streams and line-by-line file reading. Module 8 covers them with promises; the mechanics are the ones here plus await.

Common mistakes

  • Expecting the body to run at call time; it runs at the first next().
  • Reusing a generator object for a second loop (single-use); call the generator function again.
  • Forgetting the priming next() when sending values in.
  • Spreading an infinite generator.
  • Returning a value and expecting it in [...gen] — return values are not yielded.
  • Arrow functions with * (syntax error) or yield inside a nested normal function (it is not the generator).

Interview angle

  • "What is a generator?" A function that pauses at yield and resumes on next(), producing an iterator lazily.
  • "Implement take(n, iterable)." A generator with a counter that returns when the count runs out.
  • "Why lazy?" Compute only what is consumed; works on infinite and streaming sources; no intermediate arrays.
  • "What does yield* do?" Delegates to another iterable, yielding its values and forwarding next/return/throw.
  • "How do you send a value into a generator?" gen.next(v) — it becomes the value of the paused yield; prime first.

Key takeaways

  • function* + yield: the body runs on next(), pausing at each yield; return ends it; generator objects are single-use iterators and iterables.
  • Infinite sequences are fine; take/map/filter generators make lazy pipelines that compute only what is consumed.
  • yield* delegates (recursion, chaining any iterables) and forwards the protocol.
  • next(v) sends values in (prime first); return/throw finish or raise inside; finally for cleanup.
  • Generator methods are the idiomatic class iterable; async generators pair with for await.

Common questions

What does yield* do in JavaScript?

yield* inner delegates to another iterable: it yields every value of inner as if written inline, forwards next(value), return() and throw() into it, and evaluates to the return value of inner. It makes recursive traversals, such as flattening a nested array, a few lines long.

How do you pass a value into a generator?

Call gen.next(value): the argument becomes the result of the yield expression the generator is paused on. The first next() has no paused yield to receive it, so its argument is dropped — which is why a generator that takes input is primed with one empty next() first.

Why are generators lazy?

A generator computes its next value only when next() is called, so the consumer decides how much work is done. A pipeline of filter, map and take generators can therefore run over infinite or streaming sources without building intermediate arrays, stopping as soon as enough results are taken.

Can an arrow function be a generator?

No — there is no arrow syntax for generators. Use a function* declaration or expression, a *method() in a class or object literal, or async function* for an async generator. A yield inside an ordinary function nested in a generator does not belong to the generator.

Where does a generator's return value go?

It becomes the value of the final { done: true } result. for…of and spread ignore that result, so [...gen()] never contains the return value; only manual next() calls see it, and it is the value of a yield* expression.

Exercises

A lazy pipeline that proves its laziness

The starter's naturals() generator counts how many values it has produced in pulled. Write generators take(n, it) (yields at most n items and stops pulling immediately after the last), map(fn, it), filter(pred, it), and flatten(list) (recursive with yield*). Line 1 is k m: print result=<first k squares of naturals divisible by m> pulled=<how many naturals were generated>. Line 2 is a nested JSON array: print flatten=<JSON>. Finally drive take(2, naturals()) by hand and print manual=<three JSON.stringify(gen.next()) results separated by spaces>.

Example: 3 4 then [1,[2,[3,[4]]],5] →

result=[16,64,144] pulled=12
flatten=[1,2,3,4,5]
manual={"value":1,"done":false} {"value":2,"done":false} {"done":true}

Only twelve naturals were ever produced for three results — nothing past the twelfth was touched.

Sending values in

Write a generator stats() that receives numbers through next(x) and, after each, yields { count, sum, min, max, avg }. The starter primes it with one next(). Commands: add <x> prints count=<> sum=<> min=<> max=<> avg=<2dp> from the yielded object (or add <x> ignored: generator finished if it has finished); done calls gen.return() and prints done=<r.done> final=<count> values, avg <2dp> afterwards=<JSON.stringify(gen.next(1))> (final=nothing when no values were added).

Example: add 10, add 20, add 3, done →

count=1 sum=10 min=10 max=10 avg=10.00
count=2 sum=30 min=10 max=20 avg=15.00
count=3 sum=33 min=3 max=20 avg=11.00
done=true final=3 values, avg 11.00 afterwards={"done":true}

In this module: Collections and iteration

← The iteration protocol — what for…of, spread and destructuring actually call · Symbols — unique keys and the language's extension points →