Generators & Cleanup

The Course
•
4 min read

A generator is declared with function*. Calling it returns an iterator without running its body immediately. Each next() resumes until a yield or completion. Yielded values are produced on demand; returning from the generator marks iteration done.

A for/of loop that exits early with break closes its iterator by calling return when available. A generator’s finally block can therefore release a resource or record cleanup. Merely creating an iterator and abandoning it does not guarantee prompt cleanup.

Keep resource ownership explicit. A library manually pulling an iterator must decide whether to close it on failure or cancellation. Our pool owns the iterator it obtains for one invocation, closes it when unfinished, and does not promise to make the source reusable afterward.

A finally block is not a substitute for a cancellation protocol for unrelated asynchronous work. Today’s generators are synchronous. Async iterators are a different protocol and are outside this capstone’s API.

Predict, run, and explain

Write the output and trace the important state changes before checking it. Then run the program. Each editor is independent. Async examples end with main() so this editor waits for their work.

Predict, then run JavaScript
function* numbers() {
  try {
    yield 1;
    yield 2;
  } finally {
    console.log("closed");
  }
}

for (const value of numbers()) {
  console.log(value);
  break;
}
Check your prediction
1
closed

Creation, advancement and closing are separate events

Calling a generator function creates an iterator; it does not run the body to completion. Each next resumes the suspended body until the next yield or return. Side effects therefore happen when a consumer requests values, not when the generator object is created.

A consumer that owns a resource-bearing iterator must also understand early closure. A for-of break requests cleanup, while simply retaining the iterator after one next leaves it suspended. Generator validation inside the body is likewise deferred until advancement.

Creation, advancement and closing are separate events: trace JavaScript
function* values() {
  console.log("entered");
  yield 1;
  console.log("resumed");
  yield 2;
}

const cursor = values();

console.log("created");
console.log(cursor.next().value);
console.log(cursor.next().value);
Check your prediction
created
entered
1
resumed
2

The first body log is delayed until next. After the first yield, execution resumes below that yield on the following request.

Creation, advancement and closing are separate events: contrast JavaScript
function* source() {
  try {
    yield "a";
    yield "b";
  } finally {
    console.log("released");
  }
}

const cursor = source();

console.log(cursor.next().value);
cursor.return();
console.log(cursor.next().done);
Check your prediction
a
released
true

Return closes the unfinished generator and executes finally. It does not consume b as another ordinary result. A later next reports completion.

Try the rule on new inputs

Compare normal exhaustion, for-of break and explicit return. Add a validation throw at the start of the body and identify which operation observes it: generator creation or the first next call.

Your turn

Exercise

Generators & Cleanup: practise

Write a generator yielding “first” and “second” with a finally that prints released. Consume the first value in for/of and break. Expect first then released.

Your workspace JavaScript
// Put the yields inside try and the cleanup message inside finally.
Hint 1: the idea

Cleanup runs when the unfinished iterator is closed.

Hint 2: a first step

Break the consuming loop after the first console.log.

Show one solution
function* labels() {
  try {
    yield "first";
    yield "second";
  } finally {
    console.log("released");
  }
}

for (const label of labels()) {
  console.log(label);
  break;
}
first
released

Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?

Change it and test it

Compare break with calling next once and retaining the iterator. Call return explicitly on that retained iterator and observe cleanup. Explain why a library cannot just forget an unfinished resource-owning iterator after a callback throws.

Was this article helpful?

Your response is saved on this device.