Generators are functions that can pause and resume execution using yield, producing values on demand. They implement the iterator protocol, enabling lazy evaluation, infinite sequences, and custom iteration with for...of.
Declared with function* syntax, yield pauses execution and produces a value. Calling the function returns a Generator object, not the result.
next() returns { value, done }. Generators implement both the iterator and iterable protocols, working with for...of and spread syntax.
Values are computed only when requested via next() — memory-efficient for large datasets since only one value exists at a time.
generator.next(value) sends data into the generator; yield receives it. generator.throw(error) injects errors that the generator can catch.
async function* with for await...of enables lazy asynchronous iteration — useful for streaming data, paginated APIs, and chunked file processing.
Generators are special functions that can pause their execution at any point and resume later, producing a sequence of values on demand rather than computing them all at once. They implement the iterator protocol, making them work seamlessly with for...of loops and spread syntax.
Declared with function* syntax (the asterisk can be placed anywhere between function and the name). Inside a generator, the yield keyword pauses execution and produces a value. The function's state (local variables, execution position) is preserved between pauses.
Calling a generator function does not execute its body — it returns a Generator object that conforms to the iterator protocol. The body only executes when you call .next() on the generator.
An iterator is any object with a next() method that returns { value, done }. value is the yielded value, and done is true when the generator has finished (returned). Generators automatically implement this protocol.
The iterable protocol requires a [Symbol.iterator]() method that returns an iterator. Arrays, strings, Maps, and Sets are built-in iterables. Generator objects are both iterators and iterables — they have next() and [Symbol.iterator]() returns themselves.
Generators compute values only when requested. If you have a generator that yields a million items but you only call next() three times, only three values are computed. This makes generators memory-efficient for large datasets — unlike arrays that must hold all values in memory at once.
Because values are generated on demand, generators can represent infinite sequences:
function* naturals() {
let n = 1;
while (true) yield n++;
}This never runs out of memory because it only produces one number at a time. Use it with for...of and break, or with manual next() calls.
The yield keyword can also receive values. Calling generator.next(value) passes value back into the generator as the result of the yield expression. This enables two-way communication: the generator yields data out, and the consumer sends data in. The first next() call cannot send a value (there's no yield waiting to receive it).
generator.return(value) forces the generator to finish as if it hit a return statement. generator.throw(error) throws an error inside the generator at the point of the last yield, which the generator can catch with try/catch.
You can make any object work with for...of by implementing [Symbol.iterator] as a generator:
const range = {
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) yield i;
},
start: 1, end: 5
};
for (const n of range) console.log(n); // 1, 2, 3, 4, 5async function* combines generators with async/await. They yield promises and are consumed with for await...of. Use cases include streaming data, paginated API calls, and processing large files chunk by chunk.
Generators enable lazy, on-demand value production — they pause at yield and resume at next(). They implement the iterator protocol automatically. The two main use cases are: lazy evaluation of large/infinite sequences, and making custom objects iterable. Async generators extend this to asynchronous data streams.
Fun Fact
Before async/await existed, generators were used to write asynchronous code that looked synchronous — libraries like co (by TJ Holowaychuk) would automatically advance the generator when promises resolved. This pattern directly inspired the async/await syntax that was standardized in ES2017.