Add a Timeout to fetch With AbortController
fetch has no timeout option and can wait a very long time. AbortSignal.timeout(ms) fixes that in one line, and AbortSignal.any combines it with a cancel button.
fetch has no timeout option. If the server accepts the connection and then never answers, your await fetch(...) can wait for minutes. In a browser the spinner just spins. On a server, requests pile up until something runs out. The fix is one extra option: signal: AbortSignal.timeout(ms).
The one-line fix
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
If the response hasn't arrived within 5 seconds, fetch rejects and the request is cancelled. Here it is against a local server that takes 2 seconds to answer, with a 300 ms timeout:
import http from "node:http";
const server = http.createServer((req, res) => {
setTimeout(() => res.end("finally"), 2000);
}).listen(0);
const url = `http://localhost:${server.address().port}/`;
const start = Date.now();
try {
await fetch(url, { signal: AbortSignal.timeout(300) });
} catch (error) {
console.log(error.name);
console.log(error.message);
console.log(Date.now() - start < 1000 ? "gave up in under a second" : "waited too long");
} finally {
server.closeAllConnections();
server.close();
}
TimeoutError
The operation was aborted due to timeout
gave up in under a second
Save it as .mjs and run it with Node 18 or later. The local server stands in for a slow API, so you don't depend on someone else's.
Timeout vs cancel
There are two reasons to stop a request, and they need different handling:
- Timeout: the server was too slow. Tell the user, maybe retry. The error name is
TimeoutError. - Cancel: your code or the user stopped it on purpose (they left the page, typed a new search). Show nothing. The error name is
AbortError.
Combine both with AbortSignal.any:
import http from "node:http";
const server = http.createServer((req, res) => {
setTimeout(() => res.end("finally"), 2000);
}).listen(0);
const url = `http://localhost:${server.address().port}/`;
async function load(cancelSignal, timeoutMs) {
try {
const signal = AbortSignal.any([cancelSignal, AbortSignal.timeout(timeoutMs)]);
const response = await fetch(url, { signal });
return await response.text();
} catch (error) {
if (error.name === "TimeoutError") return "Show: The server took too long. Try again.";
if (error.name === "AbortError") return "Show nothing: cancelled on purpose.";
throw error;
}
}
const userCancel = new AbortController();
setTimeout(() => userCancel.abort(), 100);
console.log(await load(userCancel.signal, 5000));
console.log(await load(new AbortController().signal, 200));
server.closeAllConnections();
server.close();
Show nothing: cancelled on purpose.
Show: The server took too long. Try again.
💡 Tip: AbortSignal.timeout and AbortSignal.any work in current browsers and in Node 20 and later. On older setups, use an AbortController and setTimeout(() => controller.abort(), ms), and clear the timer when the request finishes.
The timeout covers the body too
The signal stays active while you read the body. If the headers arrive in time but the body is a huge, slow download, await response.json() can still be aborted by the same timeout. Usually that's what you want: the timeout is for the whole operation, not just the first byte. If you want separate limits, use separate signals.
Pick a sensible number
- User-facing request: 5 to 15 seconds. Longer than that, they've already given up.
- Server-to-server call inside a request: shorter than your own request timeout, so you can still answer with a proper error.
- Big downloads or reports: longer, or no timeout but a cancel button.
âš Warning: A timeout doesn't undo anything on the server. If you timed out a "create order" request, the order may still have been created. Before retrying something like that, check whether it went through, or use an idempotency key if the API supports one.
Don't do it with Promise.race
You'll see this in older code:
await Promise.race([fetch(url), new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000))]);
It stops waiting, but the request keeps running in the background and the connection stays open. AbortSignal.timeout actually cancels it.
Which fetch calls in your current project have no timeout at all, and what happens to the user if that server stops answering?
About
Add a timeout to fetch with AbortSignal.timeout, tell a timeout apart from a user cancel, and combine both signals. After this, no request hangs forever.
- Responsible
- John G
- Last Update
- 08/28/2026
- Reading Time
- 4 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.