The native fetch() API does not include a timeout option by default. If a remote server hangs or drops packets, a fetch request can remain pending indefinitely.
The Modern Solution: AbortSignal.timeout()
In Node.js 18+ and all modern evergreen browsers, you can pass AbortSignal.timeout(ms) directly into the signal option:
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
try {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === "TimeoutError") {
console.error(`Request to ${url} timed out after ${timeoutMs}ms.`);
}
throw error;
}
}Backwards-Compatible Fallback with AbortController
If you need compatibility across older browser engines that lack AbortSignal.timeout():
async function fetchWithControllerTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(id);
return response;
} catch (error) {
clearTimeout(id);
if (error.name === "AbortError") {
throw new Error(`Request timed out after ${timeoutMs}ms.`);
}
throw error;
}
}Example Usage
async function loadDashboard() {
try {
const data = await fetchWithTimeout("https://api.example.com/stats", {}, 3000);
console.log("Data loaded:", data);
} catch (err) {
console.warn("Failed to load dashboard in time:", err.message);
}
}