JavaScript Generators Table
| Piece | What it does | Field note |
|---|---|---|
function* gen() | Generator declaration | The star makes it a factory for generator objects, not a normal function |
yield x | Produce one value and pause | Execution resumes exactly here on the next next() |
gen.next() | Drive to the next yield | Returns {value, done}; done means exhausted |
gen.next(v) | Send a value INTO the yield | The yield expression evaluates to v - two-way channel |
yield* other() | Delegate to another generator | Flattens nested generators into one sequence |
gen.return(v) | Force completion | Ends the generator early; finally blocks still run |
try/finally in gen | Cleanup on early exit | finally runs when .return() kills it - closing resources works |
for (const v of gen()) | Consume like any iterable | Infinite generators are safe with for...of + break |
function* fib() | Lazy infinite sequences | Values compute on demand - the whole sequence costs nothing until pulled |
A generator is a pausable function. Mark it with a star, and yield parks it mid-execution - local state intact - until the next next() call resumes it exactly where it stopped. The table below is the working nine: declaration, driving, delegation, cleanup, and the lazy-infinite-sequence pattern that makes generators worth learning.
Bottom line: the mechanism is one thing - pause, resume, keep state - and every use is a variation on it. Lazy sequences compute values only when pulled (an infinite Fibonacci generator costs nothing until consumed). Custom iterables plug into for...of and spread like arrays. Two-way next(value) turns the generator into a coroutine that receives data at each pause.
The honest part: generators are the right answer less often than they are a fun answer. When all the values exist and fit in memory, a plain array with the array methods is simpler to read and debug. Generators earn their keep in three niches: sequences too large or infinite to materialize, values computed on demand (pagination over an API, reading a stream), and iterables with custom production logic that arrays cannot express.
How to use
- Declare with function* and produce with yield - calling the function returns a generator object, it does not run anything yet.
- Drive it: next() advances to the next yield and returns {value, done}; for...of consumes it automatically until done.
- Delegate nested sequences with yield*, clean up with try/finally (it runs even on early .return()), and build lazy sequences that compute on pull.
Frequently asked questions
What actually happens when a generator yields?
The function's entire execution frame freezes: local variables, the instruction pointer, everything stays alive while control returns to the caller. The next next() call thaws the frame and continues from the yield expression. That is the whole magic - no callbacks, no state object you maintain by hand; the language keeps the stack frame for you. It is also why generators cost more than plain functions: each live generator holds its frame in memory until it completes or is garbage-collected.
How does next(value) send data INTO a generator?
The first next() starts execution and runs to the first yield. Every subsequent next(v) makes the WAITING yield expression evaluate to v - so const reply = yield input becomes a two-way street: the generator hands input out, receives reply in, and continues. This is the coroutine pattern, and it is how async libraries historically implemented await before the language shipped it natively. If you never pass a value, the yield simply evaluates to undefined.
When is a generator better than returning an array?
When the sequence is big, infinite, or expensive past what the consumer will actually read. A generator computing Fibonacci numbers can run forever in constant memory; the array version has to decide a limit up front. Reading a huge file line by line, walking a tree, paginating an API - generators produce each item only when asked. The flip side: arrays are random-access and reusable; a generator is a one-way stream - consume it twice and you need a function that makes a new one.
Why does for...of work on generators - and what does gen.return() do?
Generators implement the iteration protocol: they have a next() method returning {value, done}, which is the same contract arrays satisfy - so for...of, spread, and destructuring all just work. gen.return(v) ends the iteration early: done becomes true, and any finally block inside the generator runs first, which is the cleanup hook (close the file, release the lock). for...of calls .return() automatically when you break out of the loop - the reason breaking a generator loop is safe.