$ snippet sleep-timeout-abort
Sleep, timeout and cancel with AbortController
An awaitable sleep, a withTimeout() wrapper for any promise, and cancellation that actually stops the underlying fetch.
/** await sleep(500) — cancellable via an AbortSignal. */
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason);
const t = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => { clearTimeout(t); reject(signal.reason); }, { once: true });
});
}
/** Reject if `promise` takes longer than `ms`. Use with fetch(url, { signal }) to also cancel the request. */
function withTimeout(promise, ms, message = `Timed out after ${ms} ms`) {
let t;
const timeout = new Promise((_, reject) => { t = setTimeout(() => reject(new Error(message)), ms); });
return Promise.race([promise, timeout]).finally(() => clearTimeout(t));
}
/** Fetch that gives up (and closes the connection) after `ms`. */
async function fetchWithTimeout(url, options = {}, ms = 10000) {
const controller = new AbortController();
const t = setTimeout(() => controller.abort(new Error(`Request timed out after ${ms} ms`)), ms);
try {
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(t);
}
}How it works
Promise.race alone gives you a timeout that reports — but the slow operation keeps running. To actually cancel network work you need an AbortSignal, which fetch understands. Modern engines also ship AbortSignal.timeout(ms), which does the controller-plus-setTimeout dance for you: fetch(url, { signal: AbortSignal.timeout(5000) }).
Gotchas
- Always clear the timer in
finally; a leakedsetTimeoutkeeps a Node process alive and leaks memory in long-running pages. - An aborted fetch rejects with an
AbortError(or the reason you passed). Checkerr.namebefore showing a scary error for a cancellation the user initiated. - Combine multiple signals with
AbortSignal.any([a, b])(newer engines) — e.g. a user-cancel signal plus a timeout signal.
Ad slot