Performance insight: lazy evaluation reduces memory — compute one value at a time instead of loading entire dataset.
Production insight: first .next() call ignores injected values — passing data there silently fails and wastes debugging time.
Biggest mistake: treating generators like regular functions — they do not execute until .next() is called.
✦ Definition~90s read
What is Generators in JavaScript?
Generators are functions that can pause execution mid-way and resume later, returning multiple values over time instead of a single return. Unlike regular functions that run to completion, a generator function (declared with function*) returns a Generator object that conforms to both the iterable and iterator protocols.
★
Imagine you're reading a recipe book and you've placed a bookmark at step 3 so you can walk away, grab an ingredient, and come back to exactly where you left off.
Each call to .next() advances execution to the next yield expression, producing a {value, done} pair. This pause-and-resume capability solves a fundamental problem: representing sequences of values that are computed lazily, or managing asynchronous control flow without nested callbacks or promise chains.
Redux-Saga famously uses generators to let you write async flows (like API calls, race conditions, or channel operations) as if they were synchronous, with yield acting as the await point — but unlike async/await, generators give the saga middleware full control to cancel execution at any yield point, which is critical for preventing memory leaks when sagas outlive their usefulness.
In the JavaScript ecosystem, generators sit between simple iterables (arrays, maps) and full reactive streams (RxJS Observables). Arrays are eager and finite; generators are lazy and can represent infinite sequences (e.g., function* idMaker() { let i = 0; while(true) yield i++; }).
Observables handle push-based async streams with cancellation built-in, but generators are pull-based — the consumer controls when the next value arrives. This makes generators ideal for scenarios where you need manual control over iteration pace, like processing large datasets chunk-by-chunk without loading everything into memory, or implementing cooperative multitasking in single-threaded JavaScript.
However, generators alone don't handle async errors or cancellation natively — that's where libraries like Redux-Saga or co add the missing pieces.
A generator's real power emerges through two-way communication: you can pass values back into a running generator via generator.next(value), and throw errors into it via generator.throw(error). This enables advanced patterns like generator composition with yield*, which delegates iteration to another generator (or any iterable) — effectively flattening nested generator calls without manual looping.
The yield delegation is not free: each delegated yield adds a small overhead because the runtime must track the delegation chain. In hot paths (e.g., processing millions of items), you'll see measurable performance gains by inlining yields instead of using yield.
Understanding these mechanics is essential when using generators in production systems — especially in Redux-Saga, where a forgotten cancel() on a running saga generator can pin entire object graphs in memory, leading to OOM crashes that are notoriously hard to debug.
Plain-English First
Imagine you're reading a recipe book and you've placed a bookmark at step 3 so you can walk away, grab an ingredient, and come back to exactly where you left off. A JavaScript generator is that bookmark — a function that can pause itself mid-execution, hand a value back to whoever called it, and then resume from the exact same spot when told to. Unlike a normal function that runs start-to-finish in one shot, a generator says 'here's one result for now — come back when you want the next one.'
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
Most JavaScriptfunctions are all-or-nothing: they start, they compute, they return, and they're gone. That works perfectly fine for most tasks, but modern applications constantly deal with problems that are inherently sequential and potentially infinite — paginated API calls, real-time data streams, complex async workflows, and lazy computation pipelines that would blow memory if evaluated all at once. Generators were introduced in ES6 precisely to solve these problems, yet they remain one of the most underused — and misunderstood — features in the language.
The core problem generators solve is control flow ownership. With a regular function, the caller has no say in when the function pauses. With a generator, the function itself decides when to yield control back, and the caller decides when to resume it. This bidirectional communication channel — values flowing out via yield, values flowing in via next(value) — creates a cooperative multitasking primitive that underpins async/await itself under the hood, powers libraries like Redux-Saga, and enables memory-efficient data pipelines that would otherwise require loading entire datasets into memory.
By the end of this article you'll understand exactly how the generator execution model works at the V8 level, how to use yield* for generator composition, how to pass values back into a running generator, how generators relate to iterators and the Symbol.iterator protocol, and the real gotchas that bite engineers in production. You'll also have concrete patterns you can drop into a codebase today.
What Generators Actually Do — Pause, Resume, and Yield Control
A generator is a function that can pause its execution mid-way and later resume from the same point, maintaining its internal state across pauses. Unlike regular functions that run to completion in one shot, generators return an iterator object with a next() method. Each call to next() executes the generator until the next yield expression, producing a value and suspending execution. This is not syntactic sugar — it's a fundamental control-flow primitive that enables cooperative multitasking within a single thread.
The key properties that matter in practice: generators are lazy — they produce values on demand, not eagerly. They maintain a hidden execution context that persists across yields, meaning local variables survive between next() calls. The yield* expression delegates to another generator, composing iterables. In Redux-Saga, every saga is a generator function. The saga middleware calls next() on the generator, and each yielded effect (like call, put, take) is an object the middleware interprets. When the middleware resolves the effect, it calls next(result) to resume the saga with the result. This is how Redux-Saga achieves its declarative side-effect model — the generator yields a description of the effect, not the effect itself.
Use generators when you need to model sequences of asynchronous steps that must be cancellable, testable, and composable. In Redux-Saga, generators are the backbone: each saga is a generator that yields effects. The ability to cancel a saga by calling return() on its generator iterator is what makes cancellation work — the generator's finally block runs, allowing cleanup. Without generators, you'd need callback chains or promise cancellations, which are harder to compose and test. In production systems handling thousands of concurrent sagas, a forgotten cancellation means the generator iterator never gets garbage-collected, leading to a memory leak that can crash the Node process.
⚠ Generator ≠ Async/Await
Generators are not syntactic sugar for async/await. Async/await returns a promise; generators return an iterator. The saga middleware bridges the gap by interpreting yielded effects.
📊 Production Insight
Teams running long-lived sagas (e.g., polling or WebSocket listeners) often forget to cancel them on component unmount. The symptom: heap grows linearly with each mount/unmount cycle, eventually hitting the memory limit. Rule: every fork or spawn must have a corresponding cancel in a finally block or via takeLatest/takeEvery.
🎯 Key Takeaway
Generators are functions that can pause and resume, maintaining state across yields.
Redux-Saga uses generators to yield declarative effect objects, not side effects.
Cancelling a saga calls return() on its iterator — missing this causes memory leaks.
thecodeforge.io
Generators Javascript
The Anatomy of a Generator: Pausing Execution
A generator is defined using the function* syntax. When called, it doesn't execute its body immediately; instead, it returns an Iterator object. This object conforms to both the iterable and iterator protocols. The actual execution only happens when you call .next(). At every yield keyword, the function's state (including variables and the call stack) is 'frozen' in memory, only to be thawed when the next call arrives.
Because generators only compute the 'next' value when asked, they are perfect for handling infinite sequences (like Fibonacci) or massive log files without crashing your Node.js heap.
📊 Production Insight
Each generator invocation creates a new stack frame that lives until the generator is fully consumed.
If you create a generator and never call .next() to completion, the frame stays in memory.
Rule: always exhaust or explicitly return generators to avoid memory leaks.
🎯 Key Takeaway
Generators don't run until you call .next().
State is preserved between yields — local variables are not reset.
Always consume the generator fully or manually close it with .return().
Iterable Protocol (Symbol.iterator) — The Engine Behind for...of
The for...of loop, spread operator, and destructuring all rely on the Iterable Protocol. An object is iterable if it has a method at the key Symbol.iterator that returns an iterator — an object with a next() method that returns {value, done}. Generators are a special case: their iterator also has [Symbol.iterator] returning itself, making them both iterable and iterator. Understanding this protocol helps you create custom iterables for lazy sequences without writing a full generator. The flow is: consumer calls Symbol.iterator, gets an iterator, then repeatedly calls .next() until done: true.
When implementing custom iterables, ensure next() returns a fresh object each time to avoid accidental mutation. In high-frequency iteration (e.g., streaming large arrays), consider reusing a single {value, done} object and mutating it — but be aware of side effects if the consumer caches references.
🎯 Key Takeaway
for...of works on any object implementing [Symbol.iterator]. Generators implement this automatically, but you can also create manual iterables for fine-grained control.
Iterable Protocol Flow
thecodeforge.io
Generators Javascript
Two-Way Communication: Passing Values In
Generators are not one-way streets. The .next(value) method allows the caller to 'inject' a value back into the generator at the exact point where it was previously paused. This value becomes the result of the yield expression inside the generator body. This bidirectional flow is what makes libraries like Redux-Saga capable of handling complex side effects as if they were synchronous code.
function* chatBot() {
const name = yield"What is your name?";
const age = yield `Hello ${name}, how old are you?`;
return `Profile: ${name}, Age: ${age}`;
}
const bot = chatBot();
// 1. Start the generator. The first next() call cannot pass data!
console.log(bot.next().value);
// 2. Pass 'Alex' into the 'name' variable
console.log(bot.next("Alex").value);
// 3. Pass '28' into the 'age' variable
console.log(bot.next(28).value);
Many developers try to initialise a generator by passing a value to the very first .next() call. That value is ignored. Always structure your generator so the first yield is a pure output, and the second .next() starts the input cycle.
📊 Production Insight
Passing a value to the first .next() is a silent no-op — the engine discards it.
This bites teams using generators for state machines: they expect the initial value to seed the state.
Rule: always synchronise with a first yield that accepts no input, then pass values on subsequent calls.
🎯 Key Takeaway
The first .next() call starts the generator — it cannot receive data.
Every yield expression evaluates to the value passed to the next .next().
Use .next(value) to communicate back into the generator for interactive sequences.
Advanced Pattern: Generator Composition with yield*
In a production environment, you often need to delegate execution from one generator to another. The yield* expression allows a generator to delegate to another iterable object (like another generator, an array, or a string), flattening the structure automatically.
You could manually iterate over a sub-generator with for...of and yield each value, but yield* is more concise and maintains the caller's ability to pass values into the sub-generator via .next().
📊 Production Insight
Each yield* delegation creates a new nested iterator — errors inside the delegated generator propagate up.
If you forget to wrap in try/catch, an unhandled error in a sub-generator will terminate the parent generator.
Rule: wrap yield* in a try/catch if the delegated generator can throw.
🎯 Key Takeaway
yield* flattens nested iterables into a single sequence.
It's essential for modular generator composition — think of it as function composition for generators.
Errors in delegated generators bubble up: handle them at the appropriate level.
yield* Delegation Performance Note
While yield is syntactically elegant, each delegation introduces a new layer of iterator overhead. Internally, the engine creates an iterator object for the delegated iterable and calls .next() on it repeatedly. In performance-critical paths—such as streaming thousands of small arrays or deeply nested generators—this overhead can add up. A simple for...of loop that yields each value manually avoids creating the extra iterator and is often faster. However, yield is still the best choice for readability and maintainability unless benchmarks prove it's a bottleneck. The overhead is typically microseconds per delegation, so only swap to manual loops after profiling.
function* efficientCompose() {
// Manual loop: one yield per element, no extra iterator objectfor (const val of [1, 2, 3, 4, 5]) {
yield val;
}
}
function* syntacticCompose() {
yield* [1, 2, 3, 4, 5]; // Cleaner, but creates an iterator for the array
}
The performance difference between yield* and a manual loop is negligible for most workloads. Only refactor if you're delegating tens of thousands of items per second and your heap profile shows high iterator allocation.
📊 Production Insight
In Redux-Saga, excessive yield delegation in watcher sagas can lead to a large number of active generator frames. Each frame holds its own closure scope. If you chain many small generators via yield, monitor the heap for generator-related allocations. Use yield* with moderate depth (<=5) to keep frame overhead under control.
🎯 Key Takeaway
yield* is clean and maintainable, but creates intermediate iterator objects. In hot code paths, a manual for...of loop may be more performant. Always measure before optimizing.
Async Generators: Handling Streams of Async Data
When you combine generators with Promises, you get async function* — an async generator. Each yield can produce a Promise, and the consumer uses for await...of to pause until each Promise resolves. This is the standard pattern for streaming data from APIs, reading files line by line with Node.js streams, or processing paginated results without loading everything into memory.
Async generators are pull-based (consumer drives each step). Observables (RxJS) are push-based (producer emits values). Choose async generators when you want backpressure control and the consumer dictates the pace.
📊 Production Insight
Async generators pause on await inside the generator — but the generator itself returns an async iterator.
Each next() call returns a Promise, so the consumer must always use for await...of or manually await.
A common bug is forgetting await before yield in an async generator — you'll yield a pending Promise instead of its resolved value.
🎯 Key Takeaway
async function* produces an async iterable — use for await...of to consume.
Each yield in an async generator should be await-ed if it depends on an async operation.
Async generators are the go-to pattern for streaming data in modern Node.js.
When to Use Async Generators vs Observables
IfYou need backpressure handling (consumer tells producer when it's ready)
→
UseUse async generator — the consumer drives each next() call.
IfYou need to compose with operators like map, filter, debounce
→
UseUse RxJS Observables — they have a rich operator library for stream transformations.
IfYou are dealing with a finite stream that can be consumed with for-await-of
→
UseAsync generator is simpler and integrates naturally with async/await.
This compact snippet focuses on the minimal pattern you need to remember for async generators. The key difference from synchronous generators: the next() method returns a Promise, so the consumer must use for await...of (or manually await gen.next()). Always ensure that inside the async generator, any yield that depends on an async operation is preceded by await, otherwise you'll yield a pending Promise instead of the resolved value. This pattern is ideal for paginated API calls, file streaming, or database cursor iterations where memory is scarce.
Writing yield fetch(url) instead of yield await fetch(url) yields a pending Promise object to the consumer. The consumer will receive {value: Promise, done: false} instead of the resolved data. Always await before yield.
📊 Production Insight
Async generators are cancellable by calling .return() on the async iterator, which triggers any finally blocks inside the generator. In Node.js streams, use this to close file handles or database connections on early termination.
🎯 Key Takeaway
Async generators = async function* + for await...of. Each yield should await its expression. They provide lazy async iteration with backpressure.
Error Handling and Generator Lifecycle Management
Generators support error injection via .throw(error). This allows the caller to raise an exception inside the generator at the current pause point. The generator can catch it with try/catch and decide to continue or abort. Additionally, .return(value) terminates the generator early, while .return() without arguments causes done: true with value: undefined. Proper lifecycle management — ensuring generators are closed when no longer needed — is critical to avoid memory leaks in long-lived applications.
Once a generator is done (return or final yield), its internal state is released — but the iterator object itself may still hold a reference to the generator context if you keep it in a variable. Assign null to the iterator reference after finishing to allow garbage collection.
📊 Production Insight
If you call .throw() on a generator that has already completed (done: true), the error propagates immediately to the caller.
This can cause uncaught exceptions in production if you don't check done before throwing.
Rule: always guard .throw() with a check like if (!gen.next().done) gen.throw(err); or handle errors in a wrapper.
🎯 Key Takeaway
Use .throw(error) to inject errors into a generator — it's like a synchronous try/catch for two-way control flow.
.return(value) terminates the generator cleanly, setting done: true.
Always clean up generators when they're no longer needed to prevent memory leaks from closure references.
You call a regular function, it runs and returns a value. You call a generator function, and nothing runs. Instead, you get back a generator object — a suspended execution context with a .next() method. That object is iterable. And lazy. Every call to .next() advances the internal state machine until the next yield. Think of function as a factory that produces a sequence of values on demand, not a function that computes them upfront. The star is a signal: this thing pauses, yields, and resumes. It's not a function. It's a state machine constructor. The yield keyword is the pause button. Execution halts exactly there, preserving local variables, the call stack — everything. When you call generator.next(), it unpauses from that exact point. This is why generators are perfect for infinite sequences, custom iterators, and anything where you want to pull values lazily instead of computing them eagerly. The syntax function can also be written as function , but keep the star attached to function — it's a declaration of kind, not a modifier on the name.
A generator function returns an object. If you call it in a for...of loop directly without assigning to a variable, you create a new generator every iteration — infinite loop or empty sequence. Always assign to a variable first.
🎯 Key Takeaway
function* creates a factory for iterable state machines, not a function that runs immediately.
thecodeforge.io
Generators Javascript
Generators Are Iterable — But Watch the Tail
A generator object implements the iterable protocol. That means for...of, spread operator ..., and destructuring all work on it. But there's a nasty edge case: for...of stops when done becomes true. It does NOT include the final return value. If your generator uses return to emit a final value, for...of silently drops it. Always use yield for all values you want the consumer to see. Use return only for cleanup or internal termination signals. This isn't a bug — it's how the iteration protocol works. The done flag is a termination signal, not a data carrier. So if you need that last value, either change your generator to yield it, or fall back to manual .next() calls and check done yourself. This matters in production when you're consuming streams, paginated APIs, or sensor data — one missing value can corrupt your pipeline. Also: generators are single-use. Once consumed, they're exhausted. You cannot rewind them. If you need multiple passes, cache the output or wrap the generator in a reusable factory function.
TailDropTrap.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforgefunction* generateRange() {
yield1;
yield2;
return 3; // This value is lost in for...of
}
const gen = generateRange();
for (const val of gen) {
console.log(val); // 1, 2 — 3 is missing!
}
// Manual iteration catches itconst gen2 = generateRange();
let result;
while (!(result = gen2.next()).done) {
console.log(result.value); // 1, 2
}
console.log('Final:', result.value); // 3
Never use return in a generator to pass data. Use return only for state cleanup. The last yielded value is invisible to for...of, spread, and destructuring.
🎯 Key Takeaway
for...of skips the generator's return value. Yield what you want consumed, return only for internal logic.
● Production incidentPOST-MORTEMseverity: high
Generator Leak in Redux-Saga Background Task
Symptom
Memory usage in the browser grows linearly over time during a single session. After multiple logouts/logins, the tab becomes unresponsive and eventually crashes with an OOM error.
Assumption
Engineers assumed that when the Redux store is cleared, all running sagas are automatically cancelled. They also assumed that generators, being functions, are garbage-collected when the reference is lost.
Root cause
The saga was started with takeEvery and never yielded to a cancellation effect like take('LOGOUT'). The generator continued running indefinitely, holding references to large objects (WebSocket message buffers) that prevented garbage collection. The generator's internal state (closed-over variables) kept the entire closure chain alive.
Fix
Add a race between the async task and a cancel channel. Use redux-saga's race effect to listen for a LOGOUT action and cancel the generator via cancel() when it fires, or use takeLatest which automatically cancels previous instances.
Key lesson
Generators hold strong references to their local scope — they are not garbage-collected until the iterator is exhausted or explicitly returned.
Always provide a cancellation path for long-running sagas. Use race, takeLatest, or a dedicated cancel channel.
Memory profiling in production should check for active generator frames — they're invisible in typical heap snapshots unless you inspect closures.
Production debug guideSymptom → Action guide for the most common generator issues in production4 entries
Symptom · 01
Generator yields undefined or skips values
→
Fix
Check if you're passing a value to the first .next() call — that value is silently ignored. Log every .next() invocation with its argument and the returned {value, done}.
Symptom · 02
Generator never finishes (infinite loop without yield)
→
Fix
Verify every iteration path has a yield or a return. A while(true) without a yield will block the event loop. Add a debug yield to inspect state.
Symptom · 03
Generator stops yielding after an error
→
Fix
Wrap the generator body in a try/catch. Use generator.throw(error) to inject an exception at the pause point — the generator can catch it and continue.
Symptom · 04
Async generator doesn't produce values in expected order
→
Fix
Async generators yield Promises. Use for await...of to consume them. Check that the yield expression is await-ed inside async function*.
★ Generator Debug Cheat SheetQuick commands and code snippets for diagnosing generator behaviour in the browser or Node.js
Generator not executing body−
Immediate action
Check that you called the generator function (not just referenced it) and that you called `.next()` at least once.
Commands
const gen = myGenerator(); console.log(gen.next());
console.log(gen.next('test').value);
Fix now
Wrap the call in console.log and check if the first line of the generator body prints.
Unexpected `done: true` before expected values+
Immediate action
Check for early `return` statements. A `return` inside a generator terminates it completely.
Commands
function* test() { yield 1; return; yield 2; }
console.log([...test()]); // [1] — yield 2 never runs
Fix now
Replace return with yield if you want to continue producing values.
Generator leaks memory (stack keeps growing)+
Immediate action
Check for recursive yields without termination. Each recursive call adds a frame.
Generators provide a cooperative multitasking model by yielding control back to the caller.
2
They are fundamentally lazy—execution only progresses when the consumer calls .next().
3
They are the secret sauce behind async/await (which is essentially a generator wrapped in a promise-runner).
4
Use yield* for clean, modular generator composition.
5
They are excellent for processing large datasets in chunks to maintain a low memory footprint.
6
Always handle generator lifecycle
memory leaks occur when generators are never exhausted or returned.
Common mistakes to avoid
5 patterns
×
Using generators as constructors
Symptom
Calling new myGenerator() throws a TypeError because generator functions are not constructible.
Fix
Never use the new keyword with a generator function. Call it normally: const gen = myGenerator().
×
Forgetting the first .next() limitation
Symptom
Passing a value to the first .next() call — the value is silently ignored, causing unexpected undefined when you expected it to be received.
Fix
Always use the first .next() without arguments. Pass the initial value to a parameter of the generator function instead, or use a second .next() call.
×
Overusing generators for simple logic
Symptom
Code becomes harder to read and debug. A simple loop or direct function would suffice.
Fix
Only use generators when you need lazy evaluation, state preservation between calls, or two-way communication. For straightforward iterations, use for...of or Array.map().
×
Exhausting the iterator and caching it
Symptom
Calling .next() after the generator is done returns { value: undefined, done: true }. If you cache the generator reference, subsequent usage yields no values.
Fix
Recreate the generator each time you need a fresh sequence, or check done before each .next() call. Use [...gen] for one-off consumption.
×
Forgetting to await yields in async generators
Symptom
The async generator yields Promises instead of resolved values, causing confusion when consuming with for await...of.
Fix
Always await the result of async operations before yielding: yield await fetch(url).
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Explain the Iterator Protocol and how generators implement it implicitly...
Q02SENIOR
Implement an infinite Fibonacci sequence using a generator and explain w...
Q03SENIOR
What is 'External Iteration' vs 'Internal Iteration', and which one do g...
Q04SENIOR
How does a generator-based async runner (like the 'co' library) work? Im...
Q05SENIOR
What happens to local variables inside a generator when it is in a 'susp...
Q06SENIOR
Compare and contrast Generators with Observables (RxJS). When would you ...
Q01 of 06SENIOR
Explain the Iterator Protocol and how generators implement it implicitly.
ANSWER
The Iterator Protocol defines a standard way to produce a sequence of values: an object with a next() method that returns { value, done }. Generators implement this protocol implicitly — every generator returns an object with a next() method that conforms to the protocol. Additionally, the generator object also has a [Symbol.iterator] method, making it iterable. This dual conformance is why you can spread a generator with [...gen] and use it in for...of loops.
Q02 of 06SENIOR
Implement an infinite Fibonacci sequence using a generator and explain why it doesn't cause a Stack Overflow.
ANSWER
``javascript
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
`
It doesn't cause a stack overflow because each call to .next() executes only the portion of the generator between yields. The generator maintains state in local variables (not the call stack), so the stack is reused on every call. The infinite while(true)` loop never recurses — it just continues from the saved point.
Q03 of 06SENIOR
What is 'External Iteration' vs 'Internal Iteration', and which one do generators facilitate?
ANSWER
External iteration means the consumer controls the iteration by calling .next() explicitly — the loop is outside the iterable. Internal iteration means the iterable controls the iteration, like Array.forEach() — the callback is called internally. Generators facilitate external iteration: the caller decides when to advance, which gives fine-grained control and enables lazy evaluation.
Q04 of 06SENIOR
How does a generator-based async runner (like the 'co' library) work? Implement a basic version using Promises.
ANSWER
An async runner takes a generator that yields Promises. It recursively calls .next() on the generator, passing the resolved value back. When the generator returns, the runner resolves the final promise. Basic implementation:
``javascript
function run(genFn) {
const gen = genFn();
function step(resolvedValue) {
const result = gen.next(resolvedValue);
if (result.done) return Promise.resolve(result.value);
return Promise.resolve(result.value).then(step);
}
return step();
}
``
This is how async/await works internally (generator + promise runner).
Q05 of 06SENIOR
What happens to local variables inside a generator when it is in a 'suspended' state?
ANSWER
Local variables are preserved in the generator's execution context, which is stored in the heap (not the stack) when the generator yields. The engine keeps a reference to the function's lexical environment, including all variables and the current program counter. When .next() is called, the engine restores this context and continues execution. This is why generators can have closures that outlive their creation scope.
Q06 of 06SENIOR
Compare and contrast Generators with Observables (RxJS). When would you choose one over the other?
ANSWER
Generators are pull-based (consumer requests values) and synchronous by default. Observables are push-based (producer pushes values) and natively asynchronous. Choose generators when you need backpressure, finite streams, or synchronous iteration. Choose Observables when you need event streams, operators (map, filter, debounce), or cancellation via unsubscription. For async data streams, async generators can compete with Observables when backpressure is required.
01
Explain the Iterator Protocol and how generators implement it implicitly.
SENIOR
02
Implement an infinite Fibonacci sequence using a generator and explain why it doesn't cause a Stack Overflow.
SENIOR
03
What is 'External Iteration' vs 'Internal Iteration', and which one do generators facilitate?
SENIOR
04
How does a generator-based async runner (like the 'co' library) work? Implement a basic version using Promises.
SENIOR
05
What happens to local variables inside a generator when it is in a 'suspended' state?
SENIOR
06
Compare and contrast Generators with Observables (RxJS). When would you choose one over the other?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the difference between yield and return in a generator?
yield pauses the generator and returns a value to the caller, but allows the generator to be resumed later. return sends a final value and permanently terminates the generator, setting the done property to true.
Was this helpful?
02
Can I use generators in an async environment?
Yes, these are called 'Async Generators' (async function*). Instead of next() returning a value, it returns a Promise that resolves to the next { value, done } pair. This is the standard way to handle streams of async data in modern JS.
Was this helpful?
03
Why does the first .next() call ignore arguments?
The first .next() call starts the generator from the very beginning of the function body. There is no yield keyword yet to 'catch' an incoming value. Arguments passed to the first .next() are silently ignored by the engine.
Was this helpful?
04
How do I handle errors inside a generator?
You can use standard try...catch blocks inside the generator. Alternatively, the caller can use iterator.throw(error), which will inject an exception into the generator at the current pause point, allowing the generator to handle the error internally.
Was this helpful?
05
Can generators be used for infinite loops?
Yes. Because generators produce values lazily, you can write while(true) { yield value } without freezing the program. The consumer decides when to stop asking for values.