Generators
Learn JavaScript generator functions: how function* and yield pause work, why generators are iterators, how yield* delegates, how return and throw finish or interrupt, and how infinite lazy sequences stay safe.
- 01Pause and resume a functionTrace exactly when a generator body runs and what each
yieldreturns. - 02Use generators as iteratorsFeed a generator object to
for...of, spread, and helper generators safely. - 03Control and compose flowsUse
return(),throw(),yield*, and lazy sequences without surprises.
Functions that can pause
A regular JavaScript function is like a sprint: call it, and it runs until it returns or throws. A generator function is different. It can run for a while, yield a value, pause with all its local variables still in place, and continue later when someone calls next().
Generators sit right on top of the iterator protocol from the previous lesson. A generator object has a next() method that returns { value, done }, and it is also iterable, so it works with for...of, spread, and destructuring. The difference is that you write the iterator as ordinary-looking control flow instead of hand-building a next method.
A generator is a function declared with function* whose body can pause at yield and resume later through the generator object’s next, return, or throw methods.
Imagine a storyteller who pauses at the end of every scene. They do not start over when you say “go on.” They remember the characters, the plot, and the exact place in the sentence. That is the generator mental model.
- In real life: The storyteller starts a tale
- In JavaScript: The first
next()starts the generator body - In real life: A pause after each scene
- In JavaScript: Each
yieldreturns a value and suspends the function - In real life: The storyteller remembers the exact sentence
- In JavaScript: Local variables and the source position are kept
- In real life: You say “go on”
- In JavaScript: Calling
next()resumes from the pausedyield
Where the analogy stops: A person can improvise between pauses. A generator resumes exactly where JavaScript suspended it, following the same code path unless return() or throw() interrupts.
In this lesson you will step through a real recorded run, command a live generator with buttons, delegate to another generator with yield*, and safely take values from sequences that could go on forever.
function* and yield
STEP THROUGHThe syntax is small: put a star after function, then use yield inside the body. The behavior is the big idea: calling the generator function runs none of the body. It only returns a generator object. The first next() call starts the body and runs until the first yield.
const tale = story();No line inside story has run yet.
tale.next()Run until the next yield, return, or throw.
{ value, done: false }The function is paused with its variables intact.
Step through the replay. It was recorded from real JavaScript calls, but it is not an engine debugger. The “Running in” row shows whether we are back in the script or currently inside the generator context. Change the last command to see normal resume, early return, and a thrown problem.
Predict first: what runs when story() is called? Step to see that nothing inside the body runs until next() resumes it.
script
function* story() { console.log("story starts"); try { yield "chapter 1"; console.log("after first pause"); yield "chapter 2"; console.log("after second pause"); return "the end"; } catch (error) { console.log("caught: " + error.message); return "recovered"; } finally { console.log("cleanup"); }} console.log("created");tale.next();tale.next();Two details matter for professional code. First, the argument passed to the first next(value) is ignored because no paused yield exists yet to receive it. Sending values back into generators is powerful, but this course saves it for Lesson 31.5. Second, a generator’s local variables are not global state. They live inside that one suspended generator object, similar to the local execution context you saw in the Execution contexts lesson.
Generators as iterators
INTERACTIVEA generator object is both an iterator and an iterable. Iterator means it has next(). Iterable means it has Symbol.iterator and can be consumed by for...of. For generator objects, iterator[Symbol.iterator]() === iterator is true.
| Question | Generator object | Array |
|---|---|---|
| When are values computed? | On demand, when a consumer asks | Usually already stored |
| Can it be infinite? | Yes, if the consumer stops | No practical infinite array |
| Can you loop it twice? | Not the same object after it is consumed | Yes, arrays keep their elements |
| Protocol | Iterator and iterable | Iterable, and array methods like map |
function* colors() { yield "red"; yield "blue";} const list = colors();console.log(list.next()); // { value: "red", done: false }console.log([...colors()]); // ["red", "blue"]console.log([...list]); // ["blue"] because red was already consumedThat last line surprises many people. list is the same iterator object you already advanced once, so spreading it starts from its current pause, not from the beginning. If you need a fresh pass, call the generator function again.
function* task() { console.log("start"); try { yield "first"; console.log("after first"); yield "second"; } catch (error) { console.log("caught " + error.message); } finally { console.log("cleanup"); } return "done";}Returned results
No commands yet.
Inside the body
No body logs yet.
The generator is created but untouched. Press next() to start the body.
next(), return(), and throw()
next() is the ordinary “go on” command. Two other methods let the caller close or interrupt a paused generator:
return(value)asks the generator to finish. It returns{ value, done: true }and runs pendingfinallyblocks. Ayieldinsidefinallycan pause once more before completion.throw(error)throws at the pausedyield. Atry...catchinside the generator can handle it. If the generator has never started, the throw happens immediately.
These methods make generators good citizens around cleanup. They work well with the try, catch & finally rules you already know: cleanup belongs in finally, even when iteration stops early.
JavaScript has function* declarations and expressions, but no generator arrow syntax. Also, generator functions are not constructors: new story() throws a TypeError.
yield* delegation
INTERACTIVEyield* means “yield everything from this iterable before continuing here.” It works with arrays, strings, sets, custom iterables, and other generators. When the delegate is a generator, its final return value becomes the value of the yield* expression.
A host can tell part of a story, hand the microphone to a guest, and wait until the guest is done. Only then does the host continue, now knowing the guest’s final private note.
- In real life: The host starts the show
- In JavaScript: The outer generator yields its own values
- In real life: The guest takes the microphone
- In JavaScript:
yield* guest()delegates each yielded value - In real life: The guest’s final line
- In JavaScript: The delegate’s
returnvalue - In real life: The host takes the mic back
- In JavaScript: Execution resumes after
yield*
Where the analogy stops: The audience hears the guest’s yielded scenes, not the guest’s final return line unless the host chooses to yield that value afterward.
function* leaf(name) { yield name + ": start"; yield name + ": end"; return name + " finished";} function* storyTeam() { yield "host intro"; const finalLine = yield* leaf("guest"); yield "host heard " + finalLine; return "host finished";} console.log([...storyTeam()].join(" | "));host introguest: startguest: endhost heard guest finished- Final
next()result:{ value: "host finished", done: true }
yield* leaf(guest) hands the microphone to the guest generator. When the guest returns, the host hears that final line as finalLine.
yield* works with any iterable. A generator delegate can also return one final value to the delegating generator.The recursive flatten example is the same idea as the Recursion lesson: each nested array gets its own call, and yield* passes values from the deeper call to the outside consumer.
Infinite sequences
INTERACTIVEBecause generators are lazy, they can describe sequences that would be impossible to store in an array: IDs, timestamps, retry attempts, Fibonacci numbers, pages from a long search, or events from a stream. The rule is simple: an infinite generator is safe only when the consumer stops.
function* ids() { let id = 1; while (true) { console.log("compute id " + id); yield id; id += 1; }} function* take(iterable, limit) { const iterator = iterable[Symbol.iterator](); for (let count = 0; count < limit; count += 1) { const result = iterator.next(); if (result.done) return; yield result.value; }} console.log([...take(ids(), 3)].join(", "));Values: 1, 2, 3
Log:
compute id 1compute id 2compute id 3
take(ids(), 3) computes exactly three IDs.
[...ids()] or [...fibonacci()]; those would never finish.[...ids()], Array.from(ids()), and a for...of loop with no break keep asking for values forever. Use a limiter such as take(ids(), 3).
[...take(ids(), 3)]orders.map(order => order.id)[...ids()]function* paidOrderIds(orders) { ... }orders.filter(order => order.paid)Array.from(fibonacci())
Sort each expression by how it consumes values. The warnings are practical: never ask an infinite source for all values.
Where you’ll use generators
Generators are not for every loop. Reach for them when a process naturally produces a sequence over time, when building the whole array would waste memory, or when recursion needs to stream results upward.
const orders = [ { id: 1, paid: true }, { id: 2, paid: false }, { id: 3, paid: true },]; function* paidOrderIds(items) { for (const order of items) { if (order.paid) yield order.id; }} console.log([...paidOrderIds(orders)].join(", "));This example is small, but the shape scales. An eager array solution such as orders.filter(...).map(...) is perfectly fine for a short in-memory array. A generator version can be useful when orders arrive one page at a time or when later code may stop after the first match.
- Tokenizing text: yield one token at a time.
- Walking trees: yield each file, folder, or menu item as you find it.
- Generating test data: yield as many cases as the test needs.
- Lazy pipelines: combine
filter,map, andtakewithout intermediate arrays.
Misconceptions and edge cases
“Calling a generator runs it.”
Calling returns a generator object. The body waits until the first next().
“A generator object is a reusable array.”
It is a one-position-at-a-time iterator. Once consumed, it stays done. Call the generator function again for a fresh iterator.
“return() skips cleanup.”
It runs pending finally blocks. That is why generators can clean up when a loop exits early.
“yield* only works with generators.”
It works with any iterable. The special part with generator delegates is that yield* evaluates to their final return value.
“All lazy code is safer.”
Laziness saves work only if the consumer stops. Spreading an infinite generator is a bug, not an optimization.
| Command | Where it resumes | Typical result |
|---|---|---|
next() | At the start or after the paused yield | Next yielded value, or final { done: true } |
return(value) | At the paused yield, as an early close request | Usually { value, done: true }, after finally |
throw(error) | At the paused yield, as if that yield threw | Caught inside, or rethrown to the caller |
Practice: generators
5 EXERCISESWrite the three logged values separated by commas.
function* letters() {
yield "A";
yield "B";
}
const it = letters();
console.log(it.next().value);
console.log(it.next().done);
console.log(it.next().done);The first call yields A, so .value is A. The second call yields B, so .done is false. The third call is past the body, so .done is true.
Create a generator that yields odd numbers up to the limit.
function* odds(limit) {
// yield 1, 3, 5 up to limit
}
console.log([...odds(5)].join(","));function* odds(limit) {
for (let n = 1; n <= limit; n += 2) {
yield n;
}
}
console.log([...odds(5)].join(","));The loop visits 1, 3, and 5. Each yield pauses and hands one number to spread; spread stops when the generator finishes.
What is wrong with this code, and how would you limit it safely?
function* ids() {
let id = 1;
while (true) yield id++;
}
console.log([...ids()].slice(0, 3));function* ids() {
let id = 1;
while (true) yield id++;
}
function* take(iterable, limit) {
let count = 0;
for (const value of iterable) {
if (count === limit) return;
count += 1;
yield value;
}
}
console.log([...take(ids(), 3)].join(","));The bug is [...ids()]: spread asks the infinite generator for every value before .slice can run. take(ids(), 3) stops after three values.
Predict the output of the delegated program.
function* one() {
yield "x";
return "done";
}
function* two() {
const message = yield* one();
yield message;
}
console.log([...two()].join("|"));The outer generator yields x from the delegate. Then yield* evaluates to done, stores it in message, and the outer generator yields that value too.
Write a generator that receives task objects like { title: "Fix nav", tags: ["ui"] } and yields only the titles whose tags include a requested tag.
function* matchingTasks(tasks, tag) {
for (const task of tasks) {
if (task.tags.includes(tag)) {
yield task.title;
}
}
}This streams matching titles without building a second task array. A caller can stop after the first few matches, or collect all of them with spread when the list is finite.
Quiz: check your understanding
7 QUESTIONSPick an answer, then read every explanation. The wrong choices name common generator mistakes.
Question 1 of 7When does a generator function body start running?
Choose an answer to see the explanation.
Question 2 of 7What does this generator sequence print?
Read the code, then predictfunction* colors() { yield "red"; yield "blue"; } const list = colors(); console.log(list.next().value); console.log(list.next().done); console.log(list.next().done);Choose an answer to see the explanation.
Question 3 of 7Which statement about generator objects is true?
Choose an answer to see the explanation.
Question 4 of 7What does
return("stop")do to a paused generator?Choose an answer to see the explanation.
Question 5 of 7What does
yield* otherIterabledo?Choose an answer to see the explanation.
Question 6 of 7What does this
yield*program print?Read the code, then predictfunction* inner() { yield "a"; return "done"; } function* outer() { const final = yield* inner(); yield final; } console.log([...outer()].join(","));Choose an answer to see the explanation.
Question 7 of 7Which line should you never run with an infinite generator?
Choose an answer to see the explanation.
Key takeaways
function*creates generator functions; calling one returns a generator object without running the body.- Each
next()resumes until the nextyield,return, or thrown error. - Generator objects are both iterators and iterables, so they work with
for...ofand spread. return()closes early and runs cleanup;throw()throws at the pausedyield.yield*delegates to another iterable and can receive a generator delegate’s final return value.- Infinite generators are safe only with a stopping consumer such as
take.
Remember the one-liner.
A generator is a pausable function that produces iterator results on demand.
Up next: Iterator helpers, lazy map, filter, take, and friends for any iterator.