A fetch() mellett könnyű olyan opciót keresni, hogy timeout: 5000. Nincs ilyen szabványos opció. A kérés határidejét és a megszakítást az AbortSignal kezeli, ami böngészőben és a beépített fetch miatt Node.js-ben is ugyanazt a mintát adja. A lényeg nem az, hogy egy ígéretet öt másodperc után elengedjünk: a folyamatban levő műveletnek is jelzést küldünk.

A timeout legyen abort, ne verseny
A gyakori kerülőút a Promise.race([fetch(...), timeout]). Ettől a hívó valóban hamarabb kap hibát, de az eredeti HTTP-kérés tovább futhat. Egy nagyobb kliensben ez felesleges kapcsolatokat, később befutó válaszokat és nehezen követhető versenyhelyzeteket hagy maga után. Az AbortController ezzel szemben közös, szabványos leállítási csatorna: ugyanaz a signal átadható a fetch-nek és más, leállítható műveletnek is.
A legegyszerűbb határidőt a AbortSignal.timeout() adja. A metódus a megadott idő után megszakított signalt állít elő. Az időzítés aktív idő: például felfüggesztett dokumentumnál nem feltétlenül telik úgy, mint egy háttérben ketyegő setTimeout. Ez UI-kérésnél általában éppen a kívánt viselkedés.
async function loadProfile(id) {
const response = await fetch(`/api/profiles/${id}`, {
signal: AbortSignal.timeout(3_000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
A timeout itt nem HTTP-státusz. A fetch elutasított promise-szal jelez; a timeout által kiváltott ok neve általában TimeoutError. Ezzel nem érdemes minden hibát összemosni: a 500-as válasz sikeresen beérkezett HTTP-válasz, ezért az ok ellenőrzése továbbra is szükséges. A fetch dokumentációja is erre a különbségre épít.
Két leállítási ok összevonása
A valós kérésnek ritkán csak határideje van. Komponens unmountol, a felhasználó új keresést indít, vagy a szerveroldali kérés klienskapcsolata megszakad. Az AbortSignal.any() egy signalba von össze több okot: az első megszakítás lezárja a műveletet. Így a hívó dönthet egyértelműen a saját leállításáról, miközben a felső korlát is megmarad.
let activeSearch;
function startSearch(query) {
const controller = new AbortController();
const signal = AbortSignal.any([
controller.signal, // új keresés vagy unmount
AbortSignal.timeout(1_500), // ne várjunk korlátlanul
]);
fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal })
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(render)
.catch((error) => {
if (signal.aborted) console.info(signal.reason.name);
else reportUnexpectedError(error);
});
return () => controller.abort(new DOMException("Elavult keresés", "AbortError"));
}
activeSearch?.();
activeSearch = startSearch(input.value);
Az any() esetén a signal.reason az elsőként abortáló signal oka; erre a DOM-szabvány is külön kitér. Saját okot ezért érdemes átadni a controller.abort(reason) hívásban. Naplózásnál hasznos, de UX-et ne kizárólag a hiba nevére építsünk: a felhasználó által indított leállítás nem hibaüzenet, a timeout viszont lehet „Próbáld újra” állapot.
Szerveroldalon a kliens eltűnése is signal
Node.js-ben ugyanez az API elérhető a globális fetch mellett; a Node dokumentációja szerint az összevont signal az első abortáló signal reason értékét örökli. Egy route handlerben a bejövő kapcsolat leállítását és a downstream szolgáltatás határidejét ugyanígy össze lehet kötni. A fontos részlet: a jelzést annak a kliensnek is át kell adni, amely tényleg dolgozik. Ha egy adatbázis-driver vagy SDK nem fogad signalt, a fetch abortálása nem fogja helyette megállítani.
A controller egyszer használatos. Abort után a hozzá tartozó signal végleg abortált marad, ezért minden új kéréshez új controllert kell létrehozni. Ugyanebből következik egy kézenfekvő szabály: ne tároljunk modulglobális controllert, és ne osszunk egyet egymástól független kérések között.
A határidő nem retry-stratégia
Az abort jó erőforrás- és állapotkezelés, de nem teszi automatikusan biztonságossá az ismétlést. Egy GET gyakran ismételhető, egy fizetést indító POST viszont lehet, hogy a szerveren már végrehajtódott, mire a kliens kifutott az időből. Ilyenkor idempotenciakulcs, állapotlekérdezés vagy alkalmazásszintű azonosító kell. A timeout csak azt mondja meg, hogy a kliens meddig vár; nem bizonyítja, hogy a távoli művelet nem történt meg.
Ha egy kliensben ma Promise.race-szal van timeout, érdemes ezt a mintát célzottan lecserélni AbortSignal.timeout()-ra. A következő lépés pedig az, hogy minden képernyő- vagy kéréséletciklusból érkező megszakítást ugyanabba a signalba fűzünk. Ettől a kód nemcsak rövidebb lesz: tényleg az a hálózati munka áll le, amelyre már nincs szükség.