Timeouts ohne setTimeout-Boilerplate kombinieren
AbortSignal.timeout() ersetzt die manuelle setTimeout-plus-AbortController-Kombination durch einen Einzeiler, und AbortSignal.any() führt mehrere Abbruchquellen zu einem einzigen Signal zusammen. Zusammen lösen beide Methoden eines der hartnäckigsten Boilerplate-Probleme in modernen Fetch-basierten Anwendungen.
Inhaltsverzeichnis
- 1. Warum AbortSignal.timeout() das Timeout-Pattern verändert
- 2. AbortSignal.timeout() im Detail: Syntax und Verhalten
- 3. Fetch-Requests mit AbortSignal.timeout() begrenzen
- 4. AbortSignal.any(): mehrere Abbruchquellen kombinieren
- 5. Timeout und manuellen Abbruch gemeinsam nutzen
- 6. Composability in wiederverwendbaren Funktionen
- 7. TimeoutError von AbortError unterscheiden
- 8. Migration von altem Timeout-Code
- 9. AbortSignal.timeout()/any() im Vergleich zum alten Muster
- 10. Zusammenfassung
- 11. FAQ
1. Warum AbortSignal.timeout() das Timeout-Pattern verändert
Vor AbortSignal.timeout() brauchte jeder zeitlich begrenzte Fetch-Request denselben Boilerplate: einen AbortController erzeugen, per setTimeout nach Ablauf einer Frist controller.abort() aufrufen, den Timer im Erfolgsfall wieder mit clearTimeout löschen und das Signal an fetch übergeben. Vier Zeilen für ein Konzept, das eigentlich nur einen einzigen Gedanken ausdrückt: „Brich diese Operation nach X Millisekunden ab.“ AbortSignal.timeout() reduziert genau das auf einen Aufruf, ohne Timer-Handle, ohne manuelles Aufräumen und ohne die Gefahr, das Löschen des Timers zu vergessen.
Der praktische Effekt zeigt sich vor allem in großen Codebasen mit vielen API-Aufrufen. Wo früher jede Funktion ihre eigene kleine Timer-Verwaltung mitschleppte, reicht heute AbortSignal.timeout(5000) als Ausdruck direkt in der Argumentliste von fetch. Das reduziert nicht nur Codezeilen, sondern auch eine ganze Klasse von Bugs, bei denen ein vergessenes clearTimeout nach erfolgreichem Request unnötig einen späteren Abbruch auslöst oder einen Timer-Handle im Speicher hält, der nie mehr gebraucht wird.
2. AbortSignal.timeout() im Detail: Syntax und Verhalten
AbortSignal.timeout(milliseconds) ist eine statische Methode auf dem globalen AbortSignal-Objekt und gibt sofort ein neues AbortSignal zurück. Intern startet die Engine einen internen Timer, der nach Ablauf der angegebenen Millisekunden das Signal in den Zustand „aborted“ versetzt. Der entscheidende Unterschied zu einem manuellen Timer: Dieser interne Timer ist an keine JavaScript-Referenz gebunden, die der Entwickler selbst verwalten müsste. Es gibt kein Handle, das man vergessen könnte zu löschen.
Das von AbortSignal.timeout() zurückgegebene Signal verhält sich in jeder Hinsicht wie ein normales AbortSignal aus einem AbortController: Es hat eine aborted-Eigenschaft, einen reason, und es feuert ein abort-Event. Der reason ist bei Ablauf automatisch eine TimeoutError-DOMException, was Fehlerbehandlung später erheblich vereinfacht, weil man Timeout-bedingte Abbrüche eindeutig von manuell ausgelösten Abbrüchen unterscheiden kann, ohne selbst einen Reason-String zu definieren.
// Old pattern: manual timer, manual cleanup, easy to forget clearTimeout
async function fetchWithManualTimeout(url) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
clearTimeout(timer); // forgetting this line leaks a timer handle
}
}
// New pattern: AbortSignal.timeout() handles the timer internally
async function fetchWithTimeout(url) {
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
return response.json();
}
3. Fetch-Requests mit AbortSignal.timeout() begrenzen
Der häufigste Einsatzort von AbortSignal.timeout() ist die signal-Option von fetch. Sobald die Frist abläuft, bricht der Browser die zugrunde liegende Netzwerkanfrage ab und die fetch-Promise verwirft mit einer DOMException vom Namen TimeoutError. Das gilt unabhängig davon, ob der Server bereits mit dem Senden der Antwort begonnen hat oder die Verbindung noch gar nicht aufgebaut wurde. In beiden Fällen wird die zugrunde liegende TCP-Verbindung sauber geschlossen, statt im Hintergrund weiterzulaufen und Ressourcen zu binden.
Ein wichtiges Detail: Der Timer von AbortSignal.timeout() startet in dem Moment, in dem die Methode aufgerufen wird, nicht erst wenn fetch tatsächlich beginnt. Bei synchronem Code zwischen Signal-Erzeugung und Request-Start ist die Differenz vernachlässigbar, aber bei komplexeren Abläufen mit vorgeschalteten asynchronen Schritten, etwa dem Warten auf einen Access Token, kann sich die effektiv verfügbare Zeit für den eigentlichen Request spürbar verkürzen. Wer das vermeiden will, erzeugt das Timeout-Signal erst unmittelbar vor dem Request-Aufruf.
4. AbortSignal.any(): mehrere Abbruchquellen kombinieren
AbortSignal.any() nimmt ein iterierbares Objekt von AbortSignal-Instanzen entgegen und gibt ein einziges kombiniertes Signal zurück, das abbricht, sobald irgendeines der übergebenen Signale abbricht. Das löst ein Problem, das vor dieser Methode nur mit manuellem Event-Listening auf jedes einzelne Signal lösbar war: Ein Request soll abbrechen, wenn entweder der Nutzer explizit einen Cancel-Button klickt, ein Timeout abläuft, oder eine übergeordnete Komponente die gesamte Operation beendet.
Vor AbortSignal.any() musste man dafür eigene Controller-Objekte bauen, die auf mehrere Quellsignale lauschten und bei jedem eintreffenden abort-Event selbst abort() auf einem zentralen Controller aufriefen. Das war fehleranfällig, weil Listener wieder entfernt werden mussten, um Memory Leaks zu vermeiden, und weil der ursprüngliche Abbruchgrund (reason) bei der Weiterleitung oft verloren ging. AbortSignal.any() übernimmt genau diese Verwaltung intern und reicht den reason des zuerst abbrechenden Signals korrekt durch.
// Combine a manual cancel button, a timeout, and a parent signal
function fetchUserProfile(userId, parentSignal) {
const userCancelController = new AbortController();
cancelButton.addEventListener('click', () => {
userCancelController.abort(new Error('User cancelled the request'));
});
const combinedSignal = AbortSignal.any([
userCancelController.signal,
AbortSignal.timeout(8000),
parentSignal,
]);
return fetch(`/api/users/${userId}`, { signal: combinedSignal })
.then((response) => response.json());
}
5. Timeout und manuellen Abbruch gemeinsam nutzen
In der Praxis treten Timeout und manueller Abbruch fast immer gemeinsam auf. Ein Suchfeld etwa soll einen laufenden Request abbrechen, sobald der Nutzer weiterschreibt, aber gleichzeitig soll ein Request auch nach einer Frist abbrechen, falls der Server ungewöhnlich langsam antwortet. Ohne AbortSignal.any() müsste man beide Fälle mit doppeltem Code abbilden, einmal für den Tastatur-Trigger und einmal für den Timer. Mit AbortSignal.any() reicht ein einziger kombinierter Signal-Parameter für die gesamte Fetch-Funktion.
Ein weiterer typischer Fall ist die Kombination aus Komponenten-Lebenszyklus und Timeout. Eine UI-Komponente, die beim Unmount alle laufenden Requests abbrechen soll, erzeugt einen eigenen AbortController, dessen Signal beim Aufräumen ausgelöst wird. Läuft parallel ein AbortSignal.timeout() für den einzelnen Request, kombiniert AbortSignal.any() beide Quellen, sodass der Request abbricht, welcher der beiden Gründe auch immer zuerst eintritt, ohne dass die Komponente selbst wissen muss, welche Quelle konkret verantwortlich war.
6. Composability in wiederverwendbaren Funktionen
Ein unterschätzter Vorteil von AbortSignal.any() ist die Composability in Bibliotheksfunktionen. Eine Funktion, die selbst eine Netzwerkanfrage macht, kann optional ein Signal des Aufrufers entgegennehmen und dieses mit einem eigenen internen Timeout kombinieren, ohne dass der Aufrufer sich um das Timeout-Detail kümmern muss. Das Ergebnis ist eine API, die sowohl von außen abbrechbar ist als auch intern robust gegen hängende Requests bleibt, mit einer einzigen Zeile Komposition.
Dieses Muster skaliert gut über mehrere Schichten hinweg. Eine High-Level-Funktion, die mehrere Low-Level-Requests orchestriert, kann ein einziges kombiniertes Signal an alle Aufrufe weiterreichen. Bricht irgendeine der Quellen ab, egal ob Nutzer-Interaktion, übergeordnetes Timeout oder ein Timeout auf Request-Ebene, brechen alle laufenden Requests konsistent ab. Das verhindert das klassische Problem, dass ein abgebrochener Vorgang im UI zwar als beendet markiert wird, im Hintergrund aber weiterhin Netzwerk-Requests laufen und Ergebnisse liefern.
7. TimeoutError von AbortError unterscheiden
Fehlerbehandlung profitiert direkt von der klaren Namensgebung, die AbortSignal.timeout() automatisch mitbringt. Ein durch AbortSignal.timeout() ausgelöster Abbruch wirft eine DOMException mit name === 'TimeoutError', während ein manueller Abbruch über controller.abort() standardmäßig name === 'AbortError' trägt, sofern kein eigener reason übergeben wurde. Diese Unterscheidung erlaubt es, im catch-Block gezielt zwischen „der Nutzer hat abgebrochen“ und „der Server war zu langsam“ zu differenzieren und jeweils passende UI-Zustände anzuzeigen.
Bei kombinierten Signalen über AbortSignal.any() trägt das resultierende Signal den reason desjenigen Ursprungssignals, das zuerst abgebrochen hat. Das bedeutet, dass der catch-Block weiterhin zwischen den unterschiedlichen Ursachen unterscheiden kann, selbst wenn drei oder vier Signale kombiniert wurden. In der Praxis reicht ein einziger switch über error.name, um Timeout, manuellen Abbruch und Parent-Abbruch sauber getrennt zu behandeln, ohne dass die kombinierende Funktion selbst zusätzliche Metadaten mitschicken muss.
async function loadDashboardData(signal) {
try {
const response = await fetch('/api/dashboard', { signal });
return await response.json();
} catch (error) {
if (error.name === 'TimeoutError') {
showToast('Server responded too slowly, please retry');
} else if (error.name === 'AbortError') {
// Silent: the user navigated away or triggered a new request
return null;
} else {
showToast('Unexpected network error');
throw error;
}
}
}
8. Migration von altem Timeout-Code
Die Migration bestehender Codebasen von manuellem setTimeout-basiertem Timeout-Handling auf AbortSignal.timeout() lässt sich meist inkrementell und risikoarm durchführen. Der erste Schritt ist eine kleine Wrapper-Funktion, die alle bisherigen fetchWithTimeout-Aufrufe im Projekt kapselt, sodass die Migration an einer einzigen Stelle stattfindet und nicht in jedem Aufrufer einzeln nachvollzogen werden muss. Bibliotheken, die intern bereits mit AbortSignal arbeiten, etwa moderne HTTP-Clients, akzeptieren die neue Signal-Erzeugung meist ohne weitere Anpassung.
Ein Punkt, den Teams beim Umstieg häufig übersehen: Browser-Support für AbortSignal.timeout() und AbortSignal.any() ist neuer als der von AbortController selbst. Node.js unterstützt beide Methoden seit Version 17.3 vollständig, moderne Browser seit Mitte 2022 beziehungsweise Ende 2023 für any(). Für Projekte mit Support älterer Browser lohnt sich ein Feature-Check mit einem einfachen Fallback auf die manuelle Timer-Variante, statt komplett auf ein Polyfill zu verzichten oder die neue API ungeprüft vorauszusetzen.
// Feature-detect before relying on the newer static methods
function createTimeoutSignal(ms) {
if (typeof AbortSignal.timeout === 'function') {
return AbortSignal.timeout(ms);
}
// Fallback for older runtimes
const controller = new AbortController();
setTimeout(() => controller.abort(), ms);
return controller.signal;
}
9. AbortSignal.timeout()/any() im Vergleich zum alten Muster
Die folgende Tabelle stellt das klassische, manuell verwaltete Timer-Pattern den beiden neuen statischen Methoden gegenüber. Der Unterschied betrifft nicht nur die Codemenge, sondern vor allem die Fehleranfälligkeit bei vergessenem Aufräumen und bei der Kombination mehrerer Abbruchquellen.
| Aufgabe | Altes Muster | Neues Muster | Vorteil |
|---|---|---|---|
| Timeout setzen | setTimeout + controller.abort() |
AbortSignal.timeout(ms) |
Kein Timer-Handle, kein clearTimeout nötig |
| Mehrere Quellen kombinieren | Manuelles Event-Listening auf jedes Signal | AbortSignal.any([...]) |
Kein Listener-Leak, reason wird durchgereicht |
| Fehlerursache erkennen | Eigener Reason-String nötig | error.name === 'TimeoutError' |
Standardisierter Name ohne eigene Konvention |
| Cleanup bei Erfolg | clearTimeout im finally-Block Pflicht | Automatisch durch die Engine | Keine vergessenen Timer-Referenzen |
| Wiederverwendbarkeit | Eigene Utility-Funktion pro Projekt | Plattform-Standard, keine Abhängigkeit | Keine externe Library nötig |
Der Vergleich zeigt, dass beide Methoden weniger neue Möglichkeiten schaffen als vielmehr bestehende, häufig fehlerhaft implementierte Muster in die Plattform selbst verlagern. AbortSignal.timeout() und AbortSignal.any() sind damit ein Beispiel dafür, wie Standardisierung Bugs verhindert, die vorher in jedem Projekt einzeln neu entstehen konnten.
Mironsoft
Moderne JavaScript-Architektur und robuste Async-Patterns
Hängende Requests und vergessene Timer im Griff?
Wir überarbeiten bestehende Fetch-Schichten, führen AbortSignal.timeout() und AbortSignal.any() konsequent ein und beseitigen fehleranfälliges Timer-Handling aus eurem Frontend.
Code-Review
Bestehende Timeout- und Abort-Implementierungen auf Schwachstellen prüfen
Refactoring
Migration auf AbortSignal.timeout()/any() ohne Breaking Changes
Architektur
Wiederverwendbare, komponierbare Async-Utilities für euer Team
10. Zusammenfassung
AbortSignal.timeout() ersetzt die manuelle Kombination aus setTimeout und AbortController durch einen einzigen Ausdruck, der ohne Timer-Handle und ohne manuelles clearTimeout auskommt. AbortSignal.any() ergänzt das um die Fähigkeit, beliebig viele Abbruchquellen zu einem einzigen Signal zu kombinieren, etwa Nutzer-Interaktion, Timeout und Komponenten-Lebenszyklus, ohne manuelles Event-Listening auf jede einzelne Quelle.
Beide Methoden liefern automatisch aussagekräftige Fehlernamen, TimeoutError beziehungsweise den durchgereichten reason des zuerst abbrechenden Signals, was die Fehlerbehandlung im catch-Block erheblich vereinfacht. Für Teams, die noch mit manuellem Timer-Handling arbeiten, ist die Migration meist ein risikoarmer, inkrementeller Schritt mit sofort spürbarem Rückgang an Boilerplate-Code.
AbortSignal.timeout() und AbortSignal.any() — Das Wichtigste auf einen Blick
Timeout ohne Timer
AbortSignal.timeout(ms) erzeugt ein Signal, das nach Ablauf automatisch abbricht. Kein clearTimeout, kein Timer-Handle.
Kombinierte Signale
AbortSignal.any([...]) bricht ab, sobald irgendeine übergebene Quelle abbricht, und reicht den Reason durch.
Fehlerunterscheidung
TimeoutError vs AbortError im catch-Block ermöglichen gezielte UI-Reaktionen ohne eigene Konvention.
Browser-Support
Node ab 17.3, moderne Browser seit 2022/2023. Feature-Check mit Fallback für ältere Umgebungen empfohlen.