Calling a function* runs no code; it returns a generator object. Each next() resumes the body until the next yield, which returns { value, done: false } and pauses with local state intact. The argument of next(x) becomes the value of the paused yield, return ends with done: true, and yield* delegates to another iterable. Generators are iterable, so for...of and spread work; Generators and yield* goes deeper.
function* idMaker(start = 0) {
let id = start;
while (true) if (yield id++) id = start; // next(true) makes yield return true
}
const ids = idMaker(1);
console.log(ids.next().value, ids.next().value, ids.next(true).value);
function* steps() { yield "a"; yield* ["b", "c"]; return "done"; } // yield* delegates
const it = steps();
it.next(); it.next(); it.next();
console.log([...steps()], it.next(), it.next());1 2 1
[ 'a', 'b', 'c' ] { value: 'done', done: true } { value: undefined, done: true }Spread ignores the return value. Values are produced lazily, so infinite generators are safe, and iterator helpers such as idMaker().take(5).toArray() have worked in all major browsers since 2025.