Skip to content

Instantly share code, notes, and snippets.

@dillonstreator
Last active April 13, 2024 20:36
Show Gist options
  • Save dillonstreator/4f030db04dfede9a17f6c97a8cd1b7f3 to your computer and use it in GitHub Desktop.
Save dillonstreator/4f030db04dfede9a17f6c97a8cd1b7f3 to your computer and use it in GitHub Desktop.
`fetch` wrapper with timeout
class TimeoutError extends Error {
error: unknown;
constructor(timeoutMs: number, error: unknown) {
super(`Operation timed out after ${timeoutMs}ms`);
this.name = 'TimeoutError';
this.error = error;
}
}
const fetchTimeout = async (
url: RequestInfo | URL,
{
timeoutMs = 5000,
fetcher = fetch,
...options
}: RequestInit & { timeoutMs?: number; fetcher?: typeof fetch } = {}
): Promise<Response> => {
const controller = new AbortController();
let timedOut = false;
const timeout = setTimeout(() => {
timedOut = true;
controller.abort();
}, timeoutMs);
const handleOgAbort = () => {
clearTimeout(timeout);
controller.abort();
};
options.signal?.addEventListener('abort', handleOgAbort);
const cleanup = () => {
clearTimeout(timeout);
options.signal?.removeEventListener('abort', handleOgAbort);
};
try {
const response = await fetcher(url, {
...options,
signal: controller.signal,
});
return response;
} catch (error) {
throw timedOut ? new TimeoutError(timeoutMs, error) : error;
} finally {
cleanup();
}
};
const ab = new AbortController();
setTimeout(() => ab.abort(), 5);
Promise.allSettled([
fetchTimeout('https://api.github.com', { timeoutMs: 15 }),
fetchTimeout('https://api.github.com', { timeoutMs: 4, signal: ab.signal }),
fetchTimeout('https://api.github.com', { timeoutMs: 5, signal: ab.signal }),
fetchTimeout('https://api.github.com', { timeoutMs: 6, signal: ab.signal }),
fetchTimeout('https://api.github.com', { timeoutMs: 250 }),
]).then(console.log);
@dillonstreator
Copy link
Author

A simpler approach that uses AbortSignal.any() and AbortSignal.timeout() methods:

const DEFAULT_TIMEOUT_MS = 5000;

const fetchTimeout = async (
    url: RequestInfo | URL,
    {
        timeoutMs = DEFAULT_TIMEOUT_MS,
        fetcher = fetch,
        ...options
    }: RequestInit & { timeoutMs?: number; fetcher?: typeof fetch } = {}
): Promise<Response> => {
    const signal = AbortSignal.any(
        [
            timeoutMs !== undefined && AbortSignal.timeout(timeoutMs),
            options.signal,
        ].filter(is(AbortSignal))
    );

    return fetcher(url, {
        ...options,
        signal,
    });
};

const is =
    <T>(instanceType: new (...args: unknown[]) => T) =>
    (instance: unknown): instance is T =>
        instance ? instance instanceof instanceType : false;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment