AbortSignal.timeout() und AbortSignal.any() im Detail
AI generated
JS
() =>
JavaScript · Async · Fetch API · Cancellation
AbortSignal.timeout() und AbortSignal.any() im Detail
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.

16 Min. Lesezeit AbortSignal.timeout · AbortSignal.any · Fetch · DOMException Chrome 103+ · Firefox 100+ · Node 17.3+

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.

11. FAQ: AbortSignal.timeout() und AbortSignal.any()

1Was macht AbortSignal.timeout() genau?
Erzeugt ein Signal, das nach Ablauf der Millisekunden automatisch abbricht, ganz ohne eigenen Timer.
2Unterschied zu manuellem setTimeout?
Kein Timer-Handle nötig, und der Abbruch trägt automatisch den Namen TimeoutError statt eines generischen AbortError.
3Was leistet AbortSignal.any()?
Kombiniert mehrere Signale zu einem, das abbricht sobald eine Quelle abbricht, inklusive durchgereichtem Reason.
4Timeout und manuellen Abbruch kombinieren?
Ja, mit AbortSignal.any() in einem Array, beide Quellen wirken gleichwertig auf dasselbe kombinierte Signal.
5Welche Runtimes unterstützen beide Methoden?
Node ab 17.3 vollständig, moderne Browser seit Mitte 2022 (timeout) und Ende 2023 (any).
6Timeout von manuellem Abbruch unterscheiden?
Über error.name: TimeoutError bei Timeout-Ablauf, AbortError bei manuellem Abbruch ohne eigenen Reason.
7Manuelles Aufräumen nötig?
Nein, die Engine verwaltet den internen Timer selbst, kein clearTimeout erforderlich.
8Bereits abgebrochenes Signal übergeben?
Das kombinierte Signal ist sofort abgebrochen und übernimmt direkt den Reason der bereits abgebrochenen Quelle.
9Fallback für ältere Umgebungen?
Feature-Check auf typeof AbortSignal.timeout und ein manueller setTimeout-Fallback, falls nicht vorhanden.
10Migration bestehender Utilities lohnenswert?
Ja, meist genügt eine zentrale Wrapper-Funktion, Aufrufer müssen in der Regel nicht angepasst werden.