Timeouts and cancellation use an AbortSignal, which fetch() and most modern APIs accept: AbortSignal.timeout(ms) aborts with a TimeoutError, controller.abort() with an AbortError, and AbortSignal.any() combines signals (both Baseline 2024). Retries wait exponentially longer, plus random jitter so clients do not retry in lockstep. A concurrency limiter lets only a few tasks run at once.
const sleep = (ms, signal) => new Promise((resolve, reject) => {
const id = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => { clearTimeout(id); reject(signal.reason); });
});
const cancel = new AbortController(); // cancel.abort() from a Cancel button
const signal = AbortSignal.any([cancel.signal, AbortSignal.timeout(20)]);
await sleep(500, signal).catch(err => console.log(err.name));
async function retry(task, retries = 3, base = 20) {
for (let i = 0; ; i++) {
try { return await task(); } catch (err) {
if (i === retries) throw err;
await sleep(base * 2 ** i * (1 + Math.random())); // exponential backoff with jitter
}
}
}
let calls = 0;
const flaky = async () => { if (++calls < 3) throw new Error("HTTP 503"); return calls; };
console.log("succeeded on call", await retry(flaky));
function limit(max, active = 0, queue = []) { // run at most `max` tasks at once
const next = () => {
if (active === max || queue.length === 0) return;
const [task, resolve, reject] = queue.shift();
active++;
Promise.try(task).then(resolve, reject).finally(() => { active--; next(); });
};
return task => new Promise((...settlers) => { queue.push([task, ...settlers]); next(); });
}
const run = limit(2);
let now = 0, peak = 0;
const job = ms => run(async () => { peak = Math.max(peak, ++now); await sleep(ms); now--; });
await Promise.all([30, 10, 40, 10, 50].map(job));
console.log("peak concurrency:", peak);TimeoutError succeeded on call 3 peak concurrency: 2
Retry only idempotent requests and transient failures (network errors, 429, 5xx), and honor a Retry-After header. For production, the MIT-licensed p-limit 2,927 (https://github.com/sindresorhus/p-limit 2,927 ) (npm 2,036 install p-limit) implements the same queue with activeCount, pendingCount and clearQueue().