async function* paginate(url) {
let page = 1;
while (true) {
const res = await fetch(`${url}?page=${page++}`);
const { items, hasMore } = await res.json();
for (const item of items) yield item;
if (!hasMore) return;
}
}
// Consumer breaks early
async function getFirst5(url) {
const results = [];
for await (const item of paginate(url)) {
results.push(item);
if (results.length === 5) break; // early break — generator may not clean up
}
return results;
}async function* paginate(url) {
const controller = new AbortController();
let page = 1;
try {
while (true) {
const res = await fetch(`${url}?page=${page++}`, {
signal: controller.signal
});
const { items, hasMore } = await res.json();
for (const item of items) yield item;
if (!hasMore) return;
}
} finally {
// Runs on break, return, or throw — cancels in-flight request
controller.abort();
console.log('Paginator cleaned up');
}
}Bug: When for...of breaks early, the generator's return() method is called. Without try/finally, in-flight fetches or open connections aren't cancelled.
Explanation: When for...of breaks early, the generator's return() is called, which runs the finally block. Using AbortController with the signal allows cancellation of in-flight fetch requests.
Key Insight: try/finally in generators is the cleanup mechanism. Always pair long-running async generators with finally to close connections, cancel requests, or release resources.