An abortable fetch with a timeout is a network request that cancels itself if it runs past a deadline, and also cancels the moment the caller asks it to — reporting a distinct error for each case. It wraps fetch with an AbortController so that a slow server, or a user who navigates away, never leaves a request hanging forever.
Implement abortableFetchTimeout(url, { timeoutMs, signal }). It returns a Promise. Internally it creates its own AbortController and calls fetch(url, { signal }) with that controller's signal. A setTimeout of timeoutMs aborts the controller and rejects with an Error whose name is "TimeoutError". If the caller passes their own signal and it aborts, you abort the controller too and reject with an Error named "AbortError". Whichever outcome happens first wins, and the losers are ignored. On every outcome you clear the timer and remove the external listener so nothing lingers.
function abortableFetchTimeout(
url: string,
options: { timeoutMs: number; signal?: AbortSignal }
): Promise<Response>;
// Fetch resolves before the deadline, so you get the Response.
const res = await abortableFetchTimeout('/api/user', { timeoutMs: 5000 });
const user = await res.json();
// The server is too slow, so the timer aborts the request.
try {
await abortableFetchTimeout('/api/slow', { timeoutMs: 3000 });
} catch (err) {
err.name; // "TimeoutError"
}
// The caller aborts (e.g. the user left the page), so the error is "AbortError".
const controller = new AbortController();
const p = abortableFetchTimeout('/api/user', {
timeoutMs: 5000,
signal: controller.signal,
});
controller.abort();
await p.catch((err) => err.name); // "AbortError"
signal both funnel into a single AbortController; fetch only listens to that one controller's signal.name === "TimeoutError"; an external abort rejects with name === "AbortError". Do not let fetch's own abort rejection surface instead.setTimeout and remove the external abort listener on every outcome, so no timer fires late and no listener leaks.signal is aborted before you start, reject with "AbortError" right away and do not call fetch.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.