Senior 7 min · March 05, 2026
Generators in JavaScript

Generator Leak in Redux-Saga — Cancel to Avoid OOM

Linear memory growth from an uncanceled generator crashes tabs.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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.
✦ 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.'

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.

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.
Generator Lifecycle in Redux-Saga THECODEFORGE.IO Generator Lifecycle in Redux-Saga From pause/resume to async streams and yield* delegation Generator Function Pauses execution at each yield Iterator Protocol Symbol.iterator drives .next() Two-Way Communication Pass values via yield expression yield* Delegation Delegates to another generator Async Generator Handles streams with for await...of ⚠ yield* creates new iterator each call Cache iterators or avoid in hot paths to prevent OOM THECODEFORGE.IO
thecodeforge.io
Generator Lifecycle in Redux-Saga
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.

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().

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.

io/thecodeforge/generators/IterableProtocol.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * io.thecodeforge - Custom iterable without generators
 */
const range = {
  from: 0,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const end = this.to;
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for (const num of range) {
  console.log(num); // 0, 1, 2, 3
}
Output
0
1
2
3
Production Insight
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
done is falsedone is truefor-of loopcalls Symbol iteratorReturns iterator objectCalls dot nextvalue and doneUse value and continueExit loop

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.

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.

io/thecodeforge/generators/DelegationPerformance.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
function* efficientCompose() {
  // Manual loop: one yield per element, no extra iterator object
  for (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
}
Benchmark Before Optimizing
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.

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.

Async Generator (for await...of) Scannable Snippet

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.

io/thecodeforge/generators/AsyncGeneratorSnippet.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function* fetchAllUsers() {
  let page = 1;
  let hasMore = true;
  while (hasMore) {
    const users = await fetch(`/api/users?page=${page++}`).then(r => r.json());
    yield users;
    if (users.length < 100) hasMore = false;
  }
}

// Usage — scannable one-liner
for await (const batch of fetchAllUsers()) {
  console.log(`Got ${batch.length} users`);
}
Missing await → Promise leak
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.

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.

Generator Functions Aren't Functions — They're Factories

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.

SequenceFactory.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge
function* fibonacciSequence() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacciSequence();
console.log(fib.next()); // { value: 0, done: false }
console.log(fib.next()); // { value: 1, done: false }
console.log(fib.next()); // { value: 1, done: false }
console.log(fib.next()); // { value: 2, done: false }
console.log(fib.next()); // { value: 3, done: false }
Output
{ value: 0, done: false }
{ value: 1, done: false }
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
Production Trap:
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.

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.thecodeforge
function* generateRange() {
  yield 1;
  yield 2;
  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 it
const gen2 = generateRange();
let result;
while (!(result = gen2.next()).done) {
  console.log(result.value); // 1, 2
}
console.log('Final:', result.value); // 3
Output
1
2
1
2
Final: 3
Production Trap:
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.
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?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Advanced JS. Mark it forged?

7 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