🔴 HardAsync Bugs🐛 Debug Challenge

Async iterator not properly cleaned up

Buggy Code — Can you spot the issue?

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;
}

Fixed Code

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 Explained

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.

More Async Bugs Debug Challenges

🟢 EasyMissing await causes wrong result🟡 MediumSequential awaits killing performance🟡 MediumSwallowed error in async function🔴 HardAsync function in forEach

Practice spotting bugs live →

38 debug challenges with AI hints

🐛 Try Debug Lab