skip to content

In JavaScript, a generator delegates with const result = yield* inner(). What does result end up holding, and what does the caller of the outer generator see?

level: middleimportance: should knowfreq 40%

answer

  1. two exits: yielded stream, returned result
  2. done: true carries the other one
  3. the consumer never sees it directly
  4. undefined unless the inner generator returns
  5. arrays and strings return nothing useful

basics

~20 s

result holds the inner generator's return value, the value carried by its final {value, done: true} result. Everything the inner generator yields goes straight through to the caller; only the return value flows back into the delegating generator.

solid answer

~50 s

A `yield*` expression evaluates to the *return* value of the iterator it delegated to — the `value` of the final `{value, done: true}` result — not to the values it yielded. Those yielded values were already handed to the outer caller as they were produced; the delegating generator never sees them. So with `function* inner() { yield 'a'; return 'done'; }`, an outer generator doing `const r = yield* inner()` gets `r === 'done'`, while the consumer saw `'a'`. If the inner generator has no explicit `return`, the expression evaluates to `undefined`, which is why most code writes `yield* inner()` as a statement and ignores the value. This split is what makes generators usable as coroutines: the yielded stream is the public output, and the return value is a private result handed back to the caller in the delegation chain.

code

javascript · 15 lines
javascript
function* inner() {
  yield 'a';
  yield 'b';
  return 'inner-result';
}

function* outer() {
  const r = yield* inner();
  yield `got ${r}`;
}

console.log([...outer()]);   // [ 'a', 'b', 'got inner-result' ]
console.log([...inner()]);   // [ 'a', 'b' ] — return value dropped
console.log([...(function* () { const r = yield* [1, 2]; yield r; })()]);
// [ 1, 2, undefined ] — an array iterator returns undefined

go deeper

for a junior

Know that a generator can both yield values and return a value, and that spreading it into an array only captures the yields.

for a middle

Explain that yield* evaluates to the inner iterator's done:true value, that it is undefined without an explicit return, and show the two-generator snippet that proves it.

for a senior

Use the split deliberately: stream output to the consumer while returning a position, count or accumulated state up the delegation chain, and know that refactoring one generator into several must not change the observed sequence.

for a principal

Be able to argue when a generator-as-coroutine design earns its keep over a plain function returning a result, and what it costs a team in readability and debuggability.

## Two different exits from a generator Every generator has two ways of producing data, and `yield*` is the place where the difference finally becomes visible. - **Yielded values** are the sequence. Each one arrives at the consumer as `{value, done: false}`. - **The return value** is the single result the generator produces when it finishes. It arrives as `{value, done: true}`. Most consumers throw the second one away. `for...of`, array spread and destructuring all stop at `done: true` and discard its `value` — that is why `[...gen()]` never contains the return value. `yield*` is the one construct that catches it. ```js function* inner() { yield 'a'; yield 'b'; return 'inner-result'; } function* outer() { const r = yield* inner(); // 'a' and 'b' went to the consumer yield `got ${r}`; // r === 'inner-result' } [...outer()]; // ['a', 'b', 'got inner-result'] ``` Notice the shape of the output: `'inner-result'` never appears on its own. It reached the consumer only because the outer generator chose to yield something built from it. ## Why the split exists Delegation is designed so that composing generators is *transparent to the consumer*. When `outer` delegates, the consumer must see exactly the values `inner` yields, unmodified and in order — otherwise you could not refactor one generator into several without changing observed behaviour. So the yielded stream belongs to the consumer. But the delegating generator often needs a result from the sub-task it just ran: how many items were emitted, where a parser stopped, the accumulated state. That channel is the return value. This is the coroutine reading of generators: `yield*` is a call into a sub-coroutine that streams output to the outside while returning a value to its caller. ```js function* readHeader(tokens) { let count = 0; for (const t of tokens) { if (t === '---') return count; // result for the caller yield t; // output for the consumer count++; } return count; } ``` ## The default is undefined A generator with no explicit `return` statement completes with `undefined`, so `yield* inner()` evaluates to `undefined` in the common case. The same is true when you delegate to something that is not a generator at all: an array's built-in iterator finishes with `{value: undefined, done: true}`, so `const r = yield* [1, 2]` leaves `r` as `undefined`. Only generators can put a meaningful value there, because only `return` in a generator body sets it. This is why the vast majority of real code writes delegation as a bare statement. Capturing the value is a deliberate act, and if you capture it from an array or a string you will get `undefined` — a classic source of confusion when someone assumes the expression is "the collected values". ## Where people go wrong - **"`yield*` returns an array of the yielded values."** It does not. If you want the values, the consumer collects them (`[...gen()]`) or the delegating generator accumulates them itself while re-yielding. - **"The return value shows up in `for...of`."** It does not. `for...of` stops when `done` becomes `true` and ignores that final `value`. You will only ever see it through a manual `next()` call or through `yield*`. - **"`return` in a generator is the same as the last `yield`."** They land in different places: one ends the sequence and carries a result, the other is part of the sequence. ```js function* g() { yield 1; return 2; } [...g()]; // [1] — the 2 is dropped const it = g(); it.next(); // { value: 1, done: false } it.next(); // { value: 2, done: true } — visible here ``` ## Practical use The pattern that actually appears in code is a chain of parsing or traversal steps, each of which streams tokens and returns a position or a count: ```js function* parse(tokens) { const headerCount = yield* readHeader(tokens); yield `header had ${headerCount} lines`; } ``` If you find yourself wanting several results back from a delegated generator, return an object — there is exactly one return-value slot per generator, and it is filled once.

  • Why does spreading a generator into an array never include its return value?
    Spread, `for...of` and destructuring all follow the iteration protocol, which stops as soon as a result has `done: true` and discards that result's `value`. The return value is only observable through a manual `next()` call or through a `yield*` that captures it. If you need it in the output, the delegating generator has to yield it explicitly.
  • What does yield* evaluate to when you delegate to an array or a Map?
    `undefined`. Built-in collection iterators complete with `{value: undefined, done: true}` — only a generator's own `return` statement puts something meaningful in that slot. So capturing the value of `yield* someArray` is always pointless, and code that does it is usually confusing the return value with the yielded elements.
  • How would you return more than one result from a delegated generator?
    Return a single object and destructure it: `const {count, lastIndex} = yield* scan(input)`. There is exactly one return-value slot per generator and it is filled once, when the body completes. Trying to smuggle extra results out by yielding them instead pollutes the sequence the consumer sees, which defeats the point of transparent delegation.

saying these in an interview costs you the question

  • Says yield* evaluates to an array of the yielded values
  • Thinks for...of surfaces the generator's return value
  • Treats return in a generator as one more yield
  • Assumes yield* over an array returns those elements
  • Believes the delegating generator sees each inner yield

context