Senior 3 min · March 05, 2026

Generator Leak in Redux-Saga — Cancel to Avoid OOM

Linear memory growth from an uncanceled generator crashes tabs.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Generators are functions that can pause execution with yield and resume later via .next().
  • They return an iterator object; state (variables, call stack) is preserved between calls.
  • function* declares a generator; yield sends a value out; .next(value) sends a value in.
  • Key components: generator function, iterator protocol (next()), done/value objects, yield* for delegation.
  • 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.
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.'

Most JavaScript functions 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.

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.

io/thecodeforge/generators/BasicGenerator.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * io.thecodeforge - Understanding the Pause/Resume Cycle
 */
function* forgeIDGenerator() {
    console.log("Generator started...");
    yield "FORGE-001";
    
    console.log("Resuming execution...");
    yield "FORGE-002";
    
    return "FORGE-COMPLETE";
}

const iterator = forgeIDGenerator();

// First call: runs until first yield
console.log(iterator.next()); // { value: 'FORGE-001', done: false }

// Second call: resumes and runs until second yield
console.log(iterator.next()); // { value: 'FORGE-002', done: false }

// Final call: runs until return
console.log(iterator.next()); // { value: 'FORGE-COMPLETE', done: true }
Output
Generator started...
{ value: 'FORGE-001', done: false }
Resuming execution...
{ value: 'FORGE-002', done: false }
{ value: 'FORGE-COMPLETE', done: true }
Forge Tip: Memory Efficiency
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().

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.

io/thecodeforge/generators/Bidirectional.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
Output
What is your name?
Hello Alex, how old are you?
Profile: Alex, Age: 28
Common Pitfall: First next() Argument
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.

io/thecodeforge/generators/Delegation.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
function* sequenceA() {
    yield 1;
    yield 2;
}

function* sequenceB() {
    yield* sequenceA(); // Delegates to sequenceA
    yield* [3, 4];      // Delegates to an Array iterable
    yield 5;
}

const gen = sequenceB();
console.log([...gen]); // Spreads all yielded values into an array
Output
[1, 2, 3, 4, 5]
Yield* vs Loop
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.

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.

io/thecodeforge/generators/AsyncGenerator.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * io.thecodeforge - Async Generator for Paginated API
 */
async function* fetchPages(url, maxPages = 5) {
    let page = 1;
    while (page <= maxPages) {
        const response = await fetch(`${url}?page=${page}`);
        const data = await response.json();
        yield data;
        page++;
    }
}

(async () => {
    for await (const pageData of fetchPages('https://api.example.com/items', 3)) {
        console.log('Received page:', pageData.length, 'items');
    }
})();
Output
Received page: 100 items
Received page: 100 items
Received page: 45 items
Async Generators vs Observables
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.

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.

io/thecodeforge/generators/ErrorHandling.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function* safeGenerator() {
    try {
        const data = yield "Fetching...";
        yield data.toUpperCase(); // May throw if data is not a string
    } catch (err) {
        yield `Error caught: ${err.message}`;
    }
    yield "Cleanup done";
}

const gen = safeGenerator();
gen.next(); // { value: 'Fetching...', done: false }

// Inject an error
console.log(gen.throw(new TypeError("Invalid data")).value);
// Output: 'Error caught: Invalid data'

console.log(gen.next().value);
// Output: 'Cleanup done'

console.log(gen.next());
// Output: { value: undefined, done: true }
Output
Fetching...
Error caught: Invalid data
Cleanup done
{ value: undefined, done: true }
Generator Lifecycle Gotcha
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.
● 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.
Commands
function* recurse() { yield* recurse(); } // infinite delegation
const gen = recurse(); gen.next(); // stack overflow eventually
Fix now
Add a base case with a simple yield or return to break recursion.
Async generator never resolves+
Immediate action
Verify that each `yield` in an async generator is awaited. Missing `await` yields a pending Promise object.
Commands
async function* asyncGen() { yield await fetch(url); }
for await (const val of asyncGen()) { console.log(val); }
Fix now
Add await before every async operation inside the async generator.
Generator vs Regular Function Comparison
FeatureRegular FunctionGenerator Function
ExecutionRun-to-completionPauses and resumes (Yields)
Return ValueA single value or PromiseAn Iterator object
Memory UsageComputes all at once (Eager)Computes on demand (Lazy)
State RetentionLost after returnMaintained while iterator is active
Error Handlingtry/catch synchronous or asynctry/catch + .throw() injection
Async Supportasync/await for promisesasync function* for streams

Key takeaways

1
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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between yield and return in a generator?
02
Can I use generators in an async environment?
03
Why does the first .next() call ignore arguments?
04
How do I handle errors inside a generator?
05
Can generators be used for infinite loops?
🔥

That's Advanced JS. Mark it forged?

3 min read · try the examples if you haven't

Previous
WeakMap and WeakSet in JavaScript
13 / 27 · Advanced JS
Next
Proxy and Reflect in JavaScript