Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

fetch() With a Deadline: AbortSignal.timeout and Friends

C
Carlo Fontanos
· 2 min read

fetch has no timeout. None. A hung server means your await hangs with it - for minutes, until the OS-level connection gives up. Every production fetch call needs a deadline, and the modern API makes it one line:

const res = await fetch('/api/report', { signal: AbortSignal.timeout(8000) });

AbortSignal.timeout(ms) builds a pre-armed signal - no AbortController ceremony, no setTimeout to clean up, and unlike the old Promise.race trick, the request is actually cancelled, freeing the connection.

Handling the failure properly

try {
    const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
} catch (err) {
    if (err.name === 'TimeoutError') return showRetry('Server took too long.');
    if (err.name === 'AbortError')   return;            // we cancelled on purpose - silence
    throw err;                                           // real network/HTTP errors
}

Three distinct outcomes, three names. Timeouts get user-facing retry UI, deliberate aborts get ignored, everything else escalates. Collapsing these into one generic catch is how "user typed too fast" ends up in your error tracker as a thousand phantom failures.

The autocomplete pattern: cancel stale requests

let controller;

input.addEventListener('input', async () => {
    controller?.abort();                    // kill the previous in-flight search
    controller = new AbortController();

    try {
        const res = await fetch(`/search?q=${encodeURIComponent(input.value)}`, {
            signal: controller.signal,
        });
        render(await res.json());
    } catch (err) {
        if (err.name !== 'AbortError') throw err;
    }
});

Without cancellation, five keystrokes launch five racing requests, and whichever lands last paints the UI - often results for an old query. Aborting the previous request makes the newest query the only query. (The full end-to-end version with debouncing and the PHP side is its own tutorial.)

Combining deadlines with cancellation: AbortSignal.any

const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(8000)]);

The request dies when either the user cancels or the deadline hits - the pattern for search-with-timeout, component unmount + deadline, and similar. One signal object also cleans up event listeners tied to the same lifecycle (that trick here).

Support: AbortSignal.timeout since Chrome 103/Firefox 100/Safari 16; .any since Chrome 116/Firefox 124/Safari 17.4. For anything older, three lines recreate timeout() with a controller and setTimeout. However you build the signal: no fetch without one, ever. Networks fail slowly; UIs shouldn't.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading