JAVASCRIPT / DESTRUCTURING, ITERABLES, AND GENERATORS
Delegating between generators with yield*
Use yield* to splice another generator or iterable into your own output, read back its return value, and write recursive generators over nested data.
What you will learn
- Replace a for...of forwarding loop with yield* to splice one generator into another
- Read a delegate's return value straight out of the yield* expression
- Write recursive generators that flatten nested data lazily
- Know that next(arg), throw() and return() travel through to the innermost delegate
Understanding Delegating between generators with yield*
A generator that writes `yield inner()` produces exactly one value: the suspended generator object itself. `yield*` does something different, it delegates. For as long as the delegate is running, every value it yields leaves the outer generator as if the outer had yielded it, and every `next(arg)`, `throw(err)` or `return(v)` the caller sends is forwarded straight through to the delegate. The mental model is a temporary splice of control flow, not a value being handed around.
`yield*` is an expression, and its value is the delegate's return value, the `value` field of the final `{ done: true }` result. That makes `return` inside a generator genuinely useful for the first time: `for...of` and spread throw that value away, but a delegating parent can read it. Iterables that are not generators work as delegates too, including strings, arrays, Sets and Maps; their iterators simply finish with `value: undefined`, so the expression evaluates to `undefined`.
The version people write by hand, `for (const v of inner()) yield v;`, produces the same values only in the simplest case. It discards the delegate's return value, it resumes `inner` with no argument no matter what the caller passed to `next(arg)`, and a `throw()` from the caller lands in the loop instead of inside `inner`. The price for getting all of that for free is that each value bubbles out through every delegating frame, so a recursion 1000 levels deep re-enters 1000 frames per item.
function* letters() {
yield 'a';
yield 'b';
return 'letters finished';
}
function* wrapper() {
yield 1;
const returned = yield* letters();
console.log('yield* evaluated to:', returned);
yield 2;
}
for (const v of wrapper()) {
console.log('got', v);
}yield* hands the entire iteration protocol over to another iterable and then evaluates to that delegate's return value.
Worked examples
Recursive flattening
A generator delegates to itself to walk arbitrarily nested arrays without building an intermediate result.
function* flatten(items) {
for (const item of items) {
if (Array.isArray(item)) {
yield* flatten(item);
} else {
yield item;
}
}
}
console.log([...flatten([1, [2, [3, [4]], 5]])].join('-'));Example explained
Line 1yield* flatten(item) starts a brand new generator for the nested array and splices its values into the stream the caller is already reading.
Line 2The 4 sits three levels down, so it passes through three delegating frames on its way out; that is where the per-value cost of deep nesting comes from.
Line 3The else branch must use plain yield because a number has no Symbol.iterator, and yield* 4 would throw a TypeError.
Sending values through a delegate
Arguments passed to next travel past the yield* and into the delegate's own paused yield.
function* adder() {
const x = yield 'first?';
const y = yield 'second?';
return x + y;
}
function* session() {
const total = yield* adder();
yield 'total=' + total;
}
const s = session();
console.log(s.next().value);
console.log(s.next(2).value);
console.log(s.next(3).value);
console.log(s.next().done);Example explained
Line 1s.next(2) is not consumed by session: yield* forwards the 2 into adder's suspended yield, so x becomes 2.
Line 2Rewriting the delegation as for (const q of adder()) yield q; would deliver the 2 to the loop's own yield and leave x undefined.
Line 3When adder returns 5, the yield* expression evaluates to 5 and session resumes on the following line with total already set.
Delegating to plain iterables
yield* accepts any iterable, and built-in iterators leave the expression's value as undefined.
function* labels() {
const r = yield* 'ab';
console.log('string delegate returned:', r);
yield* new Set(['x', 'x', 'y']);
}
for (const v of labels()) {
console.log(v);
}Example explained
Line 1yield* only requires Symbol.iterator, so strings, arrays, Sets and Maps are all valid delegates, not just generators.
Line 2A string iterator finishes with value: undefined, so r is undefined; only an explicit return inside a generator puts something useful there.
Line 3The Set has already dropped the duplicate 'x' before delegation begins, because yield* reproduces the delegate's behaviour rather than adding its own.
Important notes
Delegation ends the instant the delegate reports done, so several yield* statements in a row just run one after another; there is nothing to unwind by hand.
Every value climbs back out through each delegating frame, so recursing with yield* down a 10,000-link chain turns an O(n) walk into O(n squared); shallow trees are unaffected.
Common mistakes
Writing yield inner() instead of yield* inner(): the consumer receives a single value that is a suspended generator object, so for...of logs something like Object [Generator] {} rather than the delegate's items.
Expecting [...outer()] to contain the delegate's returned value: spread and for...of stop at done: true and discard that value, so a final result put in return instead of yield disappears unless a delegating parent reads it.
Delegating to something that is not iterable, such as a number or a plain object: it throws TypeError: ... is not iterable at the moment the yield* executes, not when the generator object is created, so the failure shows up on a later next() call.
Try it yourself
Change, predict, then run
Write walk(node) for a tree of { value, children } objects that yields every value depth-first by calling yield* walk(child) for each child. Build a three-level tree and check that [...walk(tree)].join(' ') lists the values in the order you expect.
Open the JavaScript workspaceCheck your understanding
Given function* inner() { const x = yield 'q'; return x * 2; }, version A of a wrapper uses const r = yield* inner(); yield r; and version B uses for (const v of inner()) yield v;. Each wrapper is driven with g.next() followed by g.next(5). What happens?
- A yields 'q' then 10; B yields 'q' then finishes, because the 5 never reaches inner's yield
- Both yield 'q' then 10, since for...of also forwards values sent with next
- Both yield 'q' then NaN, because a generator's return value is always discarded
- A yields 'q' then finishes and B yields 'q' then 10, since only a loop can re-yield a return value
Show answer
yield* forwards next(5) into inner's suspended yield, so x is 5, inner returns 10, and that return value becomes the value of the yield* expression, which A then yields. In B the 5 is delivered to the loop's own yield v and thrown away; the loop resumes inner with no argument, so x is undefined, inner returns NaN, and for...of discards that return value and simply ends, leaving B done. Option 1 is tempting because the two forms do emit identical values when nothing is sent inward and no return value is needed, but for...of has no channel for passing an argument into the delegate.