skip to content

A `for...of` loop over a JavaScript generator hits `break` before the generator is exhausted. What does the language do to the suspended generator, and how do you guarantee its cleanup runs?

level: seniorimportance: should knowfreq 38%

answer

  1. loops close what they open
  2. break triggers the iterator's close step
  3. resumes as if a return ran there
  4. finally is the only guaranteed cleanup
  5. after the last yield is best-effort

basics

~20 s

Leaving the loop early calls the generator's return() method, which resumes the body as though a return statement ran at the paused yield. Any enclosing finally block executes, so put resource cleanup in try/finally — code after the last yield never runs on early exit.

solid answer

~50 s

When a `for...of` loop exits early — `break`, `return`, `throw`, or a `continue` out of a labelled loop — the language performs a close step on the iterator: if it has a `return` method, that method is called. For a generator, `gen.return(v)` resumes the suspended body as if a `return v` statement appeared at the paused `yield`, which means enclosing `finally` blocks run and the generator is left completed. So the only reliable place for cleanup is a `try`/`finally` around the yields; anything written after the last `yield` simply never executes when a consumer stops early. The same close step fires for array destructuring that takes fewer elements than the generator can produce. Note that a generator suspended *before* its first statement has entered no `try` yet, so `return()` on it completes it without running any body code.

code

javascript · 16 lines
javascript
function* lines() {
  try {
    yield 'a';
    yield 'b';
    yield 'c';
  } finally {
    console.log('closing');
  }
}

for (const line of lines()) {
  if (line === 'b') break;   // logs 'closing' at the break
}

const [first] = lines();     // destructuring one element also closes it
console.log(first);          // 'a' (after logging 'closing')

go deeper

for a junior

Know that a for...of loop can break out of a generator early and that the generator does not continue afterwards. Recognising try/finally inside a generator body is enough here.

for a middle

Explain the mechanism: early exit calls the generator's return() method, which behaves like a return at the paused yield, so enclosing finally blocks run and the generator is left completed.

for a senior

Show the operational judgment — put resource release in finally, never after the last yield; call return() yourself when hand-driving a generator; and connect a handle leak under load to consumers that break out of loops early.

for a principal

Own the lifecycle contract: a generator handed across a boundary is a resource whose consumer decides when it ends, so specify who closes it, keep cleanup non-suspending, and prefer designs where abandonment cannot leak.

## Early exit is not abandonment A suspended generator is not a thread and does not keep running, but it is also not garbage the moment a consumer stops reading. It holds live state: whatever the body opened before the current `yield` is still open. If that body opened a file handle, took a lock, or started a transaction, silently walking away would leak it. The language handles this with a defined **close** step. When a `for...of` loop leaves early, the specification runs `IteratorClose` on the iterator: it looks up a `return` method on the iterator and, if one exists, calls it. Generators have one, so this is not something you opt into. ```javascript function* lines() { try { yield 'a'; yield 'b'; yield 'c'; } finally { console.log('closing'); } } for (const line of lines()) { if (line === 'b') break; // logs 'closing' before leaving the loop } ``` The `finally` block runs at the moment of the `break`, before the statement after the loop. ## What `return()` actually does `gen.return(v)` resumes the generator with a *return completion* rather than a value. Semantically it behaves as though a `return v` statement had been inserted at the point where the generator is paused. That means: - every `finally` block whose `try` the generator has entered runs, innermost first; - no more `yield`s in the normal path are reached; - the call returns `{ value: v, done: true }`, and the generator is completed; - subsequent `next()` calls return `{ value: undefined, done: true }`. Called by hand it looks like this: ```javascript const g = lines(); g.next(); // { value: 'a', done: false } console.log(g.return(0)); // logs 'closing', then { value: 0, done: true } console.log(g.next()); // { value: undefined, done: true } ``` Two edge cases are worth knowing. First, a generator that has never been started has entered no `try` block, so `return()` completes it without executing a single line of the body — if your cleanup assumption is "the finally always runs", it does not run when nothing was ever set up, which is the correct behaviour but surprises people. Second, if a `finally` block itself contains a `yield`, the generator suspends again inside the cleanup and `return()` reports `done: false`; the close is not finished until the consumer drives it through. That is an easy way to write a generator that resists closing, and a good reason to keep `finally` blocks yield-free. ## Which consumers close and which do not The close step fires for early exit from `for...of`, and also for destructuring that stops short — `const [first] = lines()` takes one element and then closes the generator, running the `finally`. Consumers that run to exhaustion (spread, `Array.from`, a `for...of` that completes normally) never need the close step: the generator finishes on its own, and its `finally` runs as part of ordinary completion. What does **not** happen automatically is cleanup for a generator you drive manually with `next()` and then simply stop using. There is no destructor hook; garbage collection does not resume a generator to run its `finally`. If you hand-drive a generator that owns a resource, you must call `return()` yourself, typically in your own `finally`: ```javascript const g = lines(); try { const first = g.next().value; // …work with first, maybe throw… } finally { g.return(); // idempotent: safe even if g is already done } ``` `return()` on an already-completed generator is harmless — it just reports `{ value, done: true }` — so this pattern is safe to apply unconditionally. ## The design rule The practical takeaway is a placement rule. Anything a generator must undo goes in a `finally` that wraps the yields; anything written after the last `yield` is best-effort only, because a consumer is free to stop at any point. Interviewers like this question because it is the moment a candidate stops treating a generator as "a lazy array" and starts treating it as a resource with a lifecycle — created, driven, and closed — where the consumer, not the producer, decides when the sequence ends. ## Diagnosing it When a service leaks handles under load and the code path involves iteration, the tell is a generator whose cleanup sits after its final `yield` while callers routinely `break` on a match or a limit. The fix is small — move the cleanup into `finally` — but you only find it if you know that leaving a loop early stops the body permanently rather than letting it fall through to the end.

  • Does spreading a generator into an array trigger the same close step?
    No, because spread never exits early — it drives the generator to exhaustion, so the body completes on its own and its `finally` runs as part of normal completion. The close step exists for consumers that stop short, such as a `for...of` with `break` or a destructuring pattern that takes fewer elements.
  • If you drive a generator manually with next() and then stop, what runs its cleanup?
    Nothing does automatically. There is no finalizer that resumes a suspended generator, so a `finally` in the body simply never executes. A hand-written driver must call `gen.return()` itself, ideally in its own `finally`; the call is safe even if the generator has already completed.
  • What happens if a generator's finally block contains a yield?
    The generator suspends inside its own cleanup. `return()` then reports `{ done: false }` with that yielded value, and the close is not complete until the consumer keeps driving it. It is a way to make a generator resist being closed, which is why `finally` blocks in generators should avoid `yield`.

saying these in an interview costs you the question

  • Assumes break just abandons the generator with no cleanup
  • Puts cleanup after the last yield and expects it to run
  • Says the generator keeps running in the background after break
  • Thinks garbage collection will run the generator's finally block
  • Believes return() is only meaningful on an exhausted generator

context