LEARN · DEBUGGING GUIDE

JavaScript Generator Iterator Not Working: Debugging Silent Failures

Generators are powerful but their lazy execution and iterator protocol nuances often cause silent failures. Here's how to diagnose and fix them with real commands and patterns.

AdvancedJavaScript8 min read

What this usually means

At its core, a generator is a function that returns an iterator. When it 'isn't working', it's usually one of: the generator function was never invoked (just defined), it hit an unhandled error inside the function that terminated it early, the caller is misusing the iterator protocol (e.g., calling `return()` or `throw()` prematurely), or a `yield*` delegation is failing because the delegated iterable isn't a proper iterator. The lazy nature of generators means side effects only happen on `.next()` calls, so a generator that looks dead may just not have been stepped through. Silent failures are common because `throw()` inside a generator is caught by the generator's own try/catch if present, or it propagates as an uncaught exception if the generator is not wrapped—but if the error is swallowed by a `finally` block that doesn't rethrow, the generator just ends.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Add a `console.log('generator started')` as the first line inside the generator function to confirm invocation.
  • 2Run the generator manually with `const gen = myGen(); gen.next();` and inspect the returned object—check `done` and `value`.
  • 3Check if the generator is being called with `new` keyword (e.g., `new myGen()`) instead of as a function call—this returns the generator object but the body doesn't execute.
  • 4Wrap the generator body in a `try/catch` and log any errors to see if an exception is silently terminating it.
  • 5If using `yield*`, verify the delegated iterable/iterator is correctly implemented and not returning `{done: true}` immediately.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchThe generator function definition—check if it's a function* declaration or expression.
  • searchCall site where the generator is invoked—look for `new` prefix or missing parentheses.
  • searchAny `try/catch/finally` blocks inside the generator—ensure errors are not swallowed.
  • searchThe consumer code that calls `.next()`, `.return()`, or `.throw()`—look for premature termination.
  • searchIf using `yield*` with custom iterables, check the `[Symbol.iterator]` method implementation.
  • searchThe scope of variables used inside the generator—closures may capture stale values.
  • searchBrowser/Node version—some older environments have incomplete generator support (e.g., pre-ES6 transpilation issues).
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningGenerator function is defined but never called (e.g., `const gen = myGen;` instead of `const gen = myGen();`).
  • warningGenerator is called with `new` (e.g., `new myGen()`) which returns the generator object but doesn't execute the body.
  • warningAn uncaught error inside the generator terminates it—no `try/catch` around the offending code.
  • warningThe caller calls `.return()` or `.throw()` on the generator before iterating fully, closing it early.
  • warning`yield*` delegates to an object that is not iterable or returns `{done: true}` prematurely.
  • warningGenerator is consumed by a `for...of` loop that breaks early, causing the generator's `return()` method to be called implicitly.
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAlways invoke generators with `myGen()` not `new myGen()`—the `new` keyword is for constructor functions, not generators.
  • buildWrap the generator body in a `try/catch` to log errors: `try { ... } catch(e) { console.error('Generator error:', e); throw e; }`.
  • buildIf a `for...of` loop must break early, manually call `gen.return()` to clean up—or redesign to avoid early exit.
  • buildFor `yield*` delegation, ensure the delegated object has a `[Symbol.iterator]` that returns a proper iterator with `next()` returning `{value, done}`.
  • buildUse `for...of` with `continue` and `break` only when you understand the implicit `return()` call—or use manual `.next()` loop.
  • buildTo prevent generator from ending on error, catch the error and yield it: `yield {error: e}` or handle in the consumer.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun the generator manually and log each `.next()` output—check that `done` becomes `true` only after you expect.
  • verifiedAdd a `console.log` inside the generator at each `yield` to confirm execution order.
  • verifiedUse `debugger;` statement inside the generator and step through in DevTools or Node inspector.
  • verifiedWrite a unit test that calls `.next()` repeatedly and asserts the sequence of values and final `done:true`.
  • verifiedIf using `yield*`, test the delegated iterator in isolation to ensure it yields the expected values.
  • verifiedCheck for any `finally` blocks that might execute early—a `finally` runs even on `return()` or `throw()`.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningAssuming the generator body runs at definition time—it only runs on `.next()` calls.
  • warningUsing `new GeneratorFunction()`—generators are not constructors; `new` returns an object but doesn't execute.
  • warningIgnoring the return value of `for...of`—you cannot break early without closing the generator.
  • warningThrowing errors inside a generator without a try/catch—the error propagates to the caller but the generator is done.
  • warningAssuming `yield*` works with any iterable—it does, but if the iterable's `next()` throws, the generator ends.
  • warningForgetting to call `.next()` after `return()`—once `return()` is called, the generator is done.
( 07 )War story

Generator silently returns empty after migrating to async iteration

Senior Backend EngineerNode.js 18, Express, MongoDB driver (cursor-based pagination)

Timeline

  1. 09:15Deploy new pagination endpoint using MongoDB cursor wrapped in a generator.
  2. 09:30Alerts: endpoint returns 200 but empty array for all requests.
  3. 09:35Check logs: no errors; generator called but yields nothing.
  4. 09:40Add console.log('start') at generator top—logs appear.
  5. 09:45Add console.log inside while(cursor.hasNext())—never prints.
  6. 09:50Inspect cursor object—it's a Promise, not resolved.
  7. 09:55Realize generator is synchronous but cursor.next() is async; generator doesn't await.
  8. 10:00Refactor generator to async generator (async function*) and fix consumer to use for await...of.
  9. 10:05Deploy fix; endpoint returns paginated data correctly.

I had converted a MongoDB cursor pagination to a generator for cleaner iteration. The cursor had a `hasNext()` method that returned a Promise. I wrote a simple while loop: `while(cursor.hasNext()) { yield await cursor.next(); }` but forgot to mark the generator as async. The generator executed synchronously: `cursor.hasNext()` returned a Promise (truthy), so the loop ran once, then `cursor.next()` returned a Promise, which I yielded as-is—the consumer received Promise objects, not actual documents. When the consumer tried to iterate with `for...of`, it got the first Promise, then the generator ended because the next `hasNext()` also returned a Promise that was truthy but the loop exited incorrectly.

I spent 20 minutes adding logs and stepping through the generator. The logs showed the generator started but the while loop condition always evaluated to true because a Promise is truthy. The yield returned a Promise, and the consumer never awaited it. I finally noticed the cursor methods returned Promises—this was an async cursor from MongoDB driver. The generator needed to be async, and the consumer needed `for await...of`.

The fix was straightforward: change `function*` to `async function*` and the consumer from `for...of` to `for await...of`. The pagination endpoint then worked correctly. The lesson: generators are synchronous by default; mixing async operations inside a synchronous generator leads to silent failures because Promises are treated as values.

Root cause

Synchronous generator used with async cursor methods—Promises were yielded instead of awaited, and the loop condition always evaluated to true.

The fix

Changed the generator to an async generator (`async function*`) and the consumer to `for await...of` loop.

The lesson

Always check if the generator is synchronous or async based on the operations inside. If your generator does any I/O or async operations, it must be an async generator. Also, beware that `while(promise)` is always truthy.

( 08 )The Iterator Protocol and Why Generators Fail Silently

A generator implements the iterator protocol: it has a `next()` method that returns `{value, done}`. The protocol is lazy—no code runs until `next()` is called. A common mistake is to forget that `return()` and `throw()` are also part of the protocol. If any consumer calls `return()` (e.g., a `for...of` loop that breaks or a `destructuring` that finishes early), the generator's `finally` block runs, and subsequent `next()` calls return `{done:true}`. This can look like the generator 'stopped working'.

To debug, wrap the generator body in a `try/catch/finally` and log every call to `next()`, `return()`, and `throw()` by overriding the generator's methods (advanced) or by using a wrapper. For example: `function wrapGen(gen) { const it = gen(); const origNext = it.next.bind(it); it.next = (...args) => { console.log('next called'); return origNext(...args); }; return it; }`. This reveals when the consumer prematurely closes the generator.

( 09 )yield* Delegation: When the Delegated Iterator Is Broken

`yield*` delegates to another iterable or iterator. If the delegated object is not a proper iterator (i.e., doesn't have a `next()` method returning `{value, done}`), the generator will throw a TypeError. But if the delegated iterator returns `{done: true}` immediately or after a few values, the generator will appear to produce fewer values than expected.

Always test the delegated iterator in isolation: `const it = delegator(); console.log(it.next()); ...`. Ensure it yields the expected sequence. Also, if the delegated iterator is a generator itself, remember that calling `.return()` on the outer generator will call `.return()` on the inner generator as well, which might close it early. Use a simple wrapper to log delegation events.

( 10 )Error Handling: Uncaught Exceptions vs. Generator Termination

An uncaught error inside a generator terminates the generator and propagates to the caller. If the caller doesn't catch it, the application crashes. However, if the caller catches it, the generator is still done—subsequent `next()` calls return `{done: true}`. This is often mistaken for a bug in the generator logic.

To prevent silent termination, wrap the generator body in a `try/catch` and decide how to handle errors: either rethrow, yield an error object, or log and continue. For example: `try { ... } catch(e) { console.error(e); yield {error: e.message}; }`. This keeps the generator alive and lets the consumer handle the error. Also, note that `finally` blocks always run, even on `return()` or `throw()`, so don't put critical cleanup in `finally` that assumes normal completion.

( 11 )Async Generators: The Silent Promise Trap

Async generators (`async function*`) return an async iterator—`next()` returns a Promise. If you use a sync `for...of` on an async generator, you'll get Promises as values. The loop will iterate over the Promises as they resolve? No—`for...of` expects synchronous iteration; it will call `next()` and get a Promise, which is truthy, so `done` is false, and the value is a Promise. This leads to unexpected behavior.

The fix is to use `for await...of` for async iterators. Also, be careful when combining sync and async generators: if you `yield*` an async generator from a sync generator, you'll get a TypeError because the sync generator expects a sync iterator. The error message is `TypeError: undefined is not a function` (or similar), which is confusing. Always match sync/sync and async/async.

Frequently asked questions

Why does my generator return `{value: undefined, done: true}` on the first call to `.next()`?

This means the generator function completed immediately without yielding any values. The most common causes: (1) the generator body has a `return` statement before any `yield`; (2) there's an uncaught error before the first `yield`; (3) you accidentally called `.return()` on the generator before calling `.next()`. Add a console.log at the start and after each yield to trace.

Can I use `new` with a generator function?

Technically yes, but it doesn't execute the generator body. `new GeneratorFunction()` returns a generator object that is not initialized—the body never runs. It's a no-op. Always call the generator as a normal function: `const gen = myGen();`.

How do I debug a generator that works in isolation but fails when consumed by a library?

Wrap the generator to log all protocol interactions. Override `next`, `return`, and `throw` to log calls and arguments. For example: `const it = myGen(); const origReturn = it.return.bind(it); it.return = (v) => { console.log('return called with', v); return origReturn(v); };`. Then pass this wrapped iterator to the library. This reveals if the library is calling `return()` early or passing unexpected arguments.

Why does `for...of` over a generator stop after one iteration even though there are more yields?

Check if the generator yields a value that is a Promise or has a `then` method—`for...of` doesn't await, so it treats the Promise as a value. The loop may continue but receive Promises. Also, if the generator body throws an error after the first yield, the loop exits. Another possibility: the generator's `return()` method is called implicitly if the loop body has a `break` or the loop is part of a destructuring pattern that finishes early.

How do I pass errors into a generator using `.throw()`?

Call `gen.throw(new Error('something'))` on the generator object. The error is thrown at the point where the generator is paused (the last `yield`). If the generator has a `try/catch` around that yield, it can handle the error and continue. If not, the error propagates to the caller and the generator is done. You can also catch it inside the generator and yield a value to continue.