skip to content

In JavaScript, what happens when you call a generator function declared with `function*` — does its body run, and what do you get back?

level: juniorimportance: must knowfreq 55%

answer

  1. calling it produces an object
  2. nothing runs until you ask
  3. first next() reaches the first yield
  4. locals survive between resumes
  5. finished stays finished

basics

~20 s

Calling a generator function runs none of its body. It returns a generator object — a paused iterator. The body advances only when you call next(), running until the next yield and then suspending with its local state intact.

solid answer

~40 s

A `function*` declaration does not produce an ordinary function; it produces a factory for generator objects. Calling it evaluates the arguments and immediately hands you back a generator object with the body suspended at the very top — no statement inside has executed yet. The first `gen.next()` starts the body and runs it until it reaches a `yield`, then freezes the whole execution context (locals, loop position, open `try` blocks) and returns `{ value: <yielded value>, done: false }`. Each later `next()` resumes exactly where it stopped. When the body returns or falls off the end you get `{ value: <returned value>, done: true }`, and every `next()` after that returns `{ value: undefined, done: true }` — a generator is one-shot and cannot be rewound.

code

javascript · 14 lines
javascript
function* counter() {
  console.log('body starts');
  yield 1;
  console.log('between yields');
  yield 2;
  return 'done';
}

const g = counter();   // nothing logged yet
console.log(g.next()); // 'body starts' then { value: 1, done: false }
console.log(g.next()); // 'between yields' then { value: 2, done: false }
console.log(g.next()); // { value: 'done', done: true }
console.log(g.next()); // { value: undefined, done: true }
console.log([...counter()]); // [ 1, 2 ] — the returned 'done' is discarded

go deeper

for a junior

Be able to say plainly that calling a function* gives you back a generator object and runs no code, and that the first next() runs up to the first yield and returns { value, done }.

for a middle

Explain what is preserved across a pause — locals, loop position, open try blocks — and why the completion value shows up as value with done: true but is dropped by for...of and spread.

for a senior

Show the practical consequences: eager argument validation needs a wrapper function, generator objects are one-shot so APIs should hand out a factory or an iterable, and laziness means unconsumed items cost nothing.

for a principal

Frame the tradeoff of exposing a pull-based generator across an API boundary — the consumer controls pace and can abandon work, but the producer holds live state until it is closed, which shapes how you document ownership and cleanup.

## What `function*` declares A function written with an asterisk — `function* gen() { … }` — is a **generator function**. The asterisk is part of the syntax, not the name, and it may sit on either side of the space (`function* gen`, `function *gen`); the first form is the house style. Object and class methods can be generators with the shorthand `*name() { … }`. Arrow functions cannot: there is no `async`-style `*=>` form. Generator functions also have no construct behaviour, so `new gen()` throws a `TypeError`. What makes a generator function different is that calling it does **not** run it. It is a factory: each call builds a fresh **generator object** whose body is suspended before the first statement. ## Calling produces an object, not a result ```javascript function* counter() { console.log('body starts'); yield 1; console.log('between yields'); yield 2; return 'done'; } const g = counter(); // nothing is logged — the body has not begun ``` Arguments are evaluated and bound to parameters at call time, but nothing in the body executes. This is a genuine source of bugs: argument validation written at the top of a generator (`if (!id) throw new TypeError(…)`) does not fire when the caller calls the generator — it fires later, when somebody first pulls from it, often in a completely different stack frame. If you want eager validation, wrap the generator in a plain function that checks arguments and then returns `inner(id)`. ## `next()` drives the body `gen.next()` resumes the body and lets it run until it hits a `yield` expression or finishes. It returns a fresh plain object each time: ```javascript console.log(g.next()); // logs 'body starts', then { value: 1, done: false } console.log(g.next()); // logs 'between yields', then { value: 2, done: false } console.log(g.next()); // { value: 'done', done: true } console.log(g.next()); // { value: undefined, done: true } ``` `value` is the operand of the `yield` that was reached; on completion it is whatever the body returned (`undefined` if it just fell off the end). `done` is `false` while the generator is suspended mid-body and `true` once it has finished. One trap worth memorising: the completion `value` is **not** part of the sequence. `for...of` and spread stop as soon as they see `done: true` and throw that value away, so `[...counter()]` is `[1, 2]`, never `[1, 2, 'done']`. If a caller needs the final result it must drive `next()` by hand and read the result object. ## What "suspended" means When a generator yields, its execution context — parameters, local variables, the position inside loops and `switch` statements, and any `try` blocks currently entered — is preserved on the generator object and taken off the call stack. The caller's stack unwinds normally; the generator is not a thread and nothing runs in the background. On the next `next()`, that context is pushed back and execution continues from the exact point of the `yield`. That retained state is the whole appeal. A hand-rolled iterator has to hoist every piece of progress into object fields and rebuild the position on every `next()`; a generator just writes the loop and lets the pause do the bookkeeping: ```javascript function* pairs(obj) { for (const key of Object.keys(obj)) { yield [key, obj[key]]; // the loop variable survives the pause } } ``` ## One-shot, and one per call A generator object is consumed as it advances. Once it reports `done: true`, it stays finished: further `next()` calls return `{ value: undefined, done: true }` rather than restarting. Each **call** of the generator function, however, produces an independent generator with its own state — so `counter()` twice gives two objects that advance separately. That is why iterable helpers usually store the generator *function* (or an object whose `[Symbol.iterator]` calls it) rather than a single generator object: the function can be called again, the object cannot be rewound. ## Why laziness is the point Because nothing runs until it is pulled, work is done per consumed item rather than up front. A generator that reads records and yields them does the read for item three only when item three is requested, and if the consumer stops early the remaining work simply never happens. That is the practical difference from a function that builds and returns an array: the array pays for everything before the caller looks at anything.

  • If a generator's body does validation at the top, when does that validation actually fail?
    Not when the caller calls the generator — that call only builds the object. The check runs on the first `next()`, so the error surfaces at the consumption site, often inside a `for...of` in unrelated code. The fix is a plain wrapper function that validates eagerly and then returns the inner generator's object.
  • Why does spreading a generator drop the value it returns?
    Spread, `for...of` and `Array.from` follow the iteration contract: they call `next()` and stop the moment a result has `done: true`, using only the `value` of results where `done` is `false`. The completion value is a signal to a manual driver, not an element of the sequence, so it is discarded.
  • Can you iterate the same generator object twice?
    No. A generator object is a one-shot iterator: once exhausted, every `next()` returns `{ value: undefined, done: true }`, so a second `for...of` over it yields nothing. Call the generator function again to get a fresh, independent generator with its own state.

saying these in an interview costs you the question

  • Thinks the body runs as soon as the generator function is called
  • Says each next() restarts the function from the top
  • Expects spread to include the value the generator returns
  • Believes an exhausted generator can be rewound by calling next() again
  • Claims a suspended generator keeps running in the background

context