Articles
JavaScript Iterator Interview Questions: Generators, Early Exit, and Cleanup
Practice JavaScript iterator interview questions with generator traces, break and return behavior, finally cleanup, and explicit async ownership boundaries.
A generator releases its resource when a loop breaks, but not when you merely stop calling next. Another generator enters finally during return and still produces a value with done: false. These traces are not exceptions to a simple rule. The simple rule was missing the iterator's suspended state and the consumer's closing behavior.
For JavaScript iterator interview questions, separate iterable creation, iterator advancement, and iterator closing. A generator packages a state machine and can implement cleanup in finally. Early loop exit usually requests return when available; continuing the loop does not. Manual abandonment, errors from advancement, and async cancellation require more precise ownership decisions.
Documented language behavior: the protocol rules below come from linked MDN references and the cited TC39 change. The traces and resource-ownership recommendations are original preparation exercises and inference, without candidate reports or employer frequency claims.
What distinguishes iterable from iterator?
An iterable provides a method at Symbol.iterator that returns an iterator. An iterator supplies next, which returns an object describing a yielded value or completion. One object can implement both protocols, but the capabilities are not identical. MDN's iteration-protocols reference defines the separate contracts.
A bare object with next is not automatically accepted by for...of if it lacks the iterable method. A generator object normally supplies both: its iterable method returns itself. That makes a generator instance a one-progress-state object rather than an automatically restartable collection.
Contrast an array with a single generator instance. Asking an array for an iterator creates an independent traversal. Asking the same generator instance for its iterator returns that instance's current state. Two consumers can therefore interfere by advancing one shared generator.
An original test should compare “reuse the same iterator” with “call a factory to create another iterator.” If the API promises replay, expose a restartable iterable or factory. If it promises a one-shot stream, make that ownership visible rather than silently sharing a cursor.
What changes when a generator is advanced?
Calling a generator function creates its iterator without immediately executing its body. The first next starts execution; a yield suspends it. A later next resumes from the suspension point. MDN's iterators-and-generators guide describes this lazy progression.
Use an original event ledger:
const events = [];
function* numbers() {
events.push('open');
try {
yield 1;
yield 2;
} finally {
events.push('close');
}
}
const it = numbers();
// events is []
it.next(); // { value: 1, done: false }
// events is ['open']
Stopping here leaves the generator suspended inside try. The existence of finally does not itself execute cleanup. The consumer still needs to resume to completion or request a supported close operation.
The generator's final return value is different from a yielded value. A next result with done: true can carry a value, but for...of does not treat that final return as another loop item. This is a useful output-prediction trap when a fixture mixes yield and return.
Why does break trigger cleanup while continue does not?
MDN's for...of documentation explains early-exit behavior and return-based cleanup. A break exits the loop and requests the iterator's return method when it exists. A continue targeting that same loop proceeds to another advancement instead of closing the iterator.
Extend the original fixture:
for (const value of numbers()) {
events.push(`read:${value}`);
break;
}
// open, read:1, close for this traversal
A return from the surrounding function and a throw from the loop body also leave the loop abruptly and can request closing. State exactly where the throw occurs. An exception thrown by the consumer body is not the same control path as an exception thrown while asking the iterator for its next result.
A custom iterator can omit return altogether. In that case the consumer has no return method to call. The language does not invent one or release an arbitrary underlying resource on the iterator author's behalf. Implement the resource contract, then test the consumer behavior against it.
Do not equate cleanup with garbage collection. A suspended object becoming unreachable does not establish timely release of an external handle. If a socket, cursor, or reader needs deterministic closing, give it an explicit owner and closing operation rather than relying on eventual memory reclamation.
The diagram follows an entered generator whose finally does not yield. The return and unstarted-generator counterexamples below explain the limits of this baseline path.
What happens when a consumer abandons an iterator manually?
This original sequence is intentionally incomplete:
const abandoned = numbers();
abandoned.next();
// No more next calls, no return call.
The close event has not occurred merely because the application no longer wants values. Manual iteration owns the responsibility that a loop normally performs on selected exits. If an application can stop early, write a try/finally around its consumer logic and invoke the documented closing operation.
For a custom iterator backed by a resource, make return idempotent where possible. Closing twice should not accidentally release a reused handle belonging to a later operation. Keep the resource instance and ownership generation explicit if the wrapper can be reset.
What if next itself throws? Do not state that every error causes the consumer to invoke return. Some protocol error paths do not perform the same closing step as a body-level exit. A generator whose own body throws while inside try runs its own finally, but that is the generator's control flow—not evidence that every custom iterator is auto-closed.
The saved custom-iterator fixture makes the boundary observable. Its next method throws Error('advance'), and its return method increments a close counter. A for...of consumer catches the advancement error, while the counter remains zero. Contrast that with the entered numbers generator: a consumer-body throw leaves the ledger as open, close.
Those are different witnesses, even though both consumers encounter an exception. The zero counter proves only that this language path did not invoke this custom return method. It does not prove the resource has already closed itself or that an unrelated driver's failure policy matches the fixture. An outer resource scope must own any required recovery.
This distinction deserves two independent tests: a generator throwing internally and a hand-written next method throwing before it returns a result. A catch around the consumer hides neither ownership obligation. Decide whether the iterator closes itself on advancement failure or whether an outer resource scope must do so.
Can return yield instead of finishing?
Generator return requests completion at the suspended position, but finally can execute intervening code. MDN's Generator.return reference includes the important case where finally itself yields.
Use this original fixture:
function* unusualCleanup() {
try {
yield 'work';
} finally {
yield 'cleanup-step';
}
}
const g = unusualCleanup();
g.next(); // work, done false
g.return('stop'); // cleanup-step, done false
g.next(); // stop, done true
The return request enters finally, but the yield suspends completion. An early-exiting for...of loop does not consume a sequence of cleanup values until done becomes true. If resource release occurs after that cleanup yield, it may remain pending when the loop exits.
For deterministic resource cleanup, keep the essential release non-yielding or define an explicit asynchronous close contract that the owner awaits. The fixture is useful precisely because it disproves “return always immediately completes a generator.” It is not a recommended cleanup API.
Calling return on a generator before its body has started does not run a finally block that was never entered. Avoid acquiring a resource outside the lazy body and assuming an unstarted generator's internal finally will own it. Acquisition and cleanup should belong to the same entered scope.
How do async iterators change the contract?
An async iterator's next returns a promise for an iteration result. for await...of awaits progression and handles early closing according to the async protocol. MDN's for-await documentation explains its async-iterator lookup and fallback to synchronous iteration.
An async generator can await a cleanup operation in finally. A consumer exiting its async loop needs to wait for closing to settle rather than logging “closed” immediately after issuing a request. Keep cleanup errors observable; swallowing them can make the resource state ambiguous.
However, an async iteration construct does not automatically cancel every in-flight network request. A producer may be awaiting an operation that does not observe an abort signal. A return request is a generator control request, not a universal interrupt mechanism for external work.
For the original network-source design exercise, pass an AbortSignal to the actual request layer, define who aborts it, and separately define how the iterator closes. A timeout should not leave both a pending request and a silently abandoned iterator. Test each layer's completion rather than assuming one keyword handles both.
There is also a version-sensitive sync fallback. Current MDN documentation says a synchronous generator yielding a rejected promise is asked to return before the rejection propagates, allowing its finally block to run. Our original Node 26.9.0 fixture did run finally, consistent with the TC39 normative change to close on rejection. This is distinct from the synchronous next method itself throwing. Older examples can describe earlier behavior; record the runtime and retain a deployment compatibility check.
How would you implement a bounded consumer?
The original task is to read until a stop token or a maximum item count, then release the source. Use a loop that has a deliberate early-exit path, and choose whether the token itself belongs in the output. The limit must be checked at the intended boundary: before requesting another value or after accepting the current one.
An off-by-one read can matter even when the returned prefix is correct. Requesting one extra value may advance a shared cursor, incur an I/O operation, or trigger a side effect. Assert the number of next calls, not just the final array of values.
Define behavior for an empty source, a token in the first item, normal exhaustion, consumer-body failure, advancement failure, and a close failure. Keep cleanup policy independent from matching policy. A correct stop-token algorithm can still leak ownership if its manual iterator wrapper omits closing.
If the consumer must resume later, closing may be the wrong operation. Some iterators can continue after a loop exits, while generators normally finish after return-based closing unless unusual finally behavior intervenes. The API must choose resumable cursor semantics or one-shot scoped iteration, not accidentally mix them.
Practice the trace and ownership boundary
| Practice question | Iterator-specific follow-up |
|---|---|
| Extract stream prefix before stop token | Count advancement calls and close on intentional early exit. |
| Design log-query stream processor | Define one-shot versus resumable cursor semantics. |
| Run Promise Tasks with Bounded Concurrency | Separate iterator completion from cancellation of pending work. |
| Build a Reliable Streaming Chat UI | Close an abandoned response stream without committing stale output. |
| Implement a robust socket message reader | Give reader lifetime and partial-message state explicit owners. |
Explain a ledger, then defend the API
Create an event list containing open, next, yield, body, return, finally, and close events. Predict the order for full exhaustion, break, continue, and a body exception. Then add manual abandonment, a next exception, and a finally that yields. Each new case should change one boundary rather than obscure the trace with real network timing.
These fixtures can establish language-level behavior in the recorded runtime. They do not establish that a production driver releases a database cursor, that a browser cancels a fetch, or that a remote server stops work. Add those integration tests only when the real resource is part of the exercise.
For JavaScript iterator interview questions, a strong answer names the suspended state and its owner. Early exit is part of correctness, not a footnote after the algorithm.
Sources and Further Reading
- MDN: Iteration protocols
- MDN: Iterators and generators
- MDN: for...of and early exit
- MDN: Generator.return and finally
- MDN: for await...of
- TC39: Close sync iterator when async wrapper yields rejection
Research note: Official technical documentation checked October 1, 2026. Original traces distinguish synchronous generators, custom iterators, and async consumers; production resource cancellation requires separate evidence.