skip to content

In a JavaScript generator function, what is the difference between yield [1, 2, 3] and yield* [1, 2, 3], and what kinds of operands does yield* accept?

level: juniorimportance: should knowfreq 45%

answer

  1. one value versus many values
  2. the star iterates its operand
  3. any iterable, not only generators
  4. strings, Maps and Sets qualify too
  5. nothing is buffered or copied

basics

~20 s

yield [1, 2, 3] produces one value: the array itself. yield* iterates its operand and produces each element separately, so the consumer sees 1, then 2, then 3. yield* accepts any iterable, not only generators.

solid answer

~40 s

`yield` hands the consumer exactly one value, whatever the expression evaluates to, so `yield [1, 2, 3]` produces a single array. `yield*` is delegation: it takes an iterator from its operand and re-yields every value that iterator produces, so `yield* [1, 2, 3]` produces three separate values. The operand only has to be iterable — an array, a string, a `Map`, a `Set`, another generator object, or any object with a `Symbol.iterator` method. While the delegation is running the outer generator is transparent: values flow straight to the caller, arguments passed to `next()` go through to the inner iterator, and the outer generator only resumes once the inner one is exhausted. That is what makes `yield*` the cheap way to splice one sequence into another without building an array first.

code

javascript · 13 lines
javascript
function* single() {
  yield [1, 2, 3];
}

function* delegating() {
  yield 0;
  yield* [1, 2, 3];
  yield* 'ab';
  yield* new Set([9, 9]);
}

console.log([...single()]);     // [ [ 1, 2, 3 ] ]
console.log([...delegating()]); // [ 0, 1, 2, 3, 'a', 'b', 9 ]

go deeper

for a junior

Be able to say plainly that yield emits one value while yield* emits each element of an iterable, and show it with a two-line snippet and a spread into an array.

for a middle

Explain that yield* obtains an iterator via Symbol.iterator, so strings, Sets, Maps and custom iterables all qualify, and that values are pulled lazily rather than copied up front.

for a senior

Show why delegating instead of concatenating matters on real data: no intermediate arrays, works on unbounded or very large sources, and the consumer controls how much is ever produced.

for a principal

Frame delegation as the composition primitive that lets a codebase build sequence utilities that stack without allocation, and be ready to say where that composability stops being worth the indirection.

## The two operators look similar and do very different things Inside a `function*`, `yield` is a single-value operator. It evaluates the expression to its right, suspends the generator, and hands that one value to whoever called `next()`. The value's type is irrelevant — an array, an object, another generator object — it is still one item in the sequence. `yield*` is the *delegation* operator. Instead of producing its operand, it treats the operand as a sequence and produces everything in it, one element at a time, as if those `yield`s had been written inline in the outer generator. ```js function* single() { yield [1, 2, 3]; } function* spread() { yield* [1, 2, 3]; } [...single()]; // [[1, 2, 3]] — one value [...spread()]; // [1, 2, 3] — three values ``` ## What counts as a valid operand `yield*` does not require a generator. The spec step is "get an iterator from the operand", which means it calls the operand's `Symbol.iterator` method. Anything iterable works: ```js function* mixed() { yield* 'ab'; // 'a', 'b' — strings are iterable yield* [1, 2]; // 1, 2 yield* new Set([3, 3, 4]); // 3, 4 yield* new Map([['k', 'v']]); // ['k', 'v'] — entries, as arrays yield* other(); // another generator object } function* other() { yield 'z'; } ``` If the operand is not iterable — a number, a plain object literal with no `Symbol.iterator`, `null` — you get a `TypeError` at the moment the delegation starts, not a silently skipped step. `yield* 42` throws. ## Delegation is transparent, not a wrapper While a `yield*` is in progress, the outer generator is effectively a pass-through pipe: - every value the inner iterator yields goes to the outer caller untouched; - an argument given to `next(v)` is forwarded to the inner iterator's `next(v)`; - `throw()` and `return()` called on the outer generator are routed to the inner iterator when it has those methods, so cleanup in the inner generator's `finally` still runs; - the outer generator resumes at the statement after the `yield*` only when the inner iterator reports `done: true`. Because of that transparency the caller cannot tell, from the values alone, whether a sequence came from one generator or five composed ones. That is exactly the property composition relies on. ## Why this matters beyond the syntax trivia The usual junior mistake is to reach for concatenation: ```js // Materializes everything, defeating the point of a generator function* bad(a, b) { yield [...a, ...b]; } // Streams both sources, allocates nothing extra function* good(a, b) { yield* a; yield* b; } ``` The second version never builds an intermediate array, works when `a` or `b` is huge, and works when one of them is an endless sequence — the consumer simply stops pulling. That is the whole reason `yield*` exists: it lets you build a sequence out of smaller sequences without ever holding the whole thing in memory. A second everyday use is recursion. A generator can delegate to itself, which turns a nested structure into a flat stream with almost no code: ```js function* leaves(node) { if (Array.isArray(node)) { for (const child of node) yield* leaves(child); } else yield node; } [...leaves([1, [2, [3, [4]]]])]; // [1, 2, 3, 4] ``` ## Things people get wrong - **"`yield*` returns an array of the inner values."** It does not return a collection at all; it produces the values one at a time. Its own expression value is the inner iterator's *return* value, which is usually `undefined`. - **"`yield*` only works on generators."** Any iterable works, which is why `yield* someArray` and `yield* someString` are common. - **"`yield` spreads arrays."** It never does; there is no implicit flattening in JavaScript's `yield`. - **"Delegation copies the inner sequence first."** Nothing is buffered. The inner iterator is advanced lazily, one `next()` at a time, driven by the outer consumer.

  • What happens if the operand of yield* is not iterable?
    A `TypeError` is thrown at the point the delegation begins, because the delegation step calls the operand's `Symbol.iterator` method and there is none. `yield* 42` and `yield* {a: 1}` both throw; `yield* []` is fine and simply produces nothing. Wrapping the value — `yield 42` — is the fix when you meant a single item.
  • If you delegate to a generator that never ends, does the outer generator ever resume?
    No. `yield*` resumes the outer generator only when the inner iterator reports `done: true`, so anything written after a delegation to an endless sequence is unreachable. That is usually harmless because the consumer stops pulling, but it means you cannot put a "footer" value after `yield* endless()` and expect it to appear.

saying these in an interview costs you the question

  • Says yield* only accepts another generator object
  • Thinks yield [1,2,3] produces three separate values
  • Claims yield* builds an array of the inner values first
  • Believes delegation buffers the inner sequence in memory
  • Says yield* on a plain object silently yields nothing

context