demo · v131

Timeout Scope

Combine AbortSignal.timeout() with await using so a fetch and all its cascading cleanup are tied to a single scope. Left panel: request completes before the deadline — the scope exits cleanly. Right panel: request exceeds the timeout — the AbortController fires, fetch is cancelled, and the resource stack unwinds automatically.

✓ completes in time
elapsed
ready
✕ exceeds timeout
elapsed
ready
AbortSignal.any(): timeout or user cancel
composed signal state
ready
click run, then either wait for timeout or cancel manually
adjust sliders and click "Run both scenarios"

the code

async function fetchWithTimeout(url, ms) {
  // The DisposableStack unwinds automatically when the scope exits
  await using stack = new AsyncDisposableStack();

  // AbortController registered as a disposable resource
  const controller = new AbortController();
  stack.defer(async () => {
    controller.abort(); // called even if fetch throws
    console.log("AbortController cleaned up");
  });

  // Race fetch against a timeout signal
  const timeoutSignal = AbortSignal.timeout(ms);
  const anySignal = AbortSignal.any([controller.signal, timeoutSignal]);

  const res = await fetch(url, { signal: anySignal });
  const data = await res.json();
  return data; // stack.disposeAsync() runs here → controller.abort()
}

// Usage
try {
  const data = await fetchWithTimeout("/api/slow", 3000);
} catch (e) {
  if (e.name === "TimeoutError") console.log("timed out — all resources disposed");
}

see also