Promise.any() praktisch nutzen: Das erste erfolgreiche Ergebnis gewinnt
AI generated
JS
() =>
JavaScript · Promises · Asynchrone Programmierung
Promise.any() praktisch nutzen
Das erste erfolgreiche Ergebnis gewinnt, Fehler werden ignoriert bis zum Schluss

Wenn mehrere Spiegel-APIs dieselbe Antwort liefern koennten und nur das schnellste erfolgreiche Ergebnis zaehlt, ist Promise.any() der richtige Kombinator, klar abgegrenzt von Promise.race() und Promise.allSettled().

13 Min. Lesezeit Promise.any() AggregateError Async/Await

1. Vier Promise-Kombinatoren, vier Szenarien

ES2020 brachte mit Promise.allSettled() und Promise.any() die letzten beiden fehlenden Bausteine im Quartett der Promise-Kombinatoren. Promise.all() wartet auf alle Erfolge und bricht beim ersten Fehler ab, Promise.race() reagiert auf das erste beliebige Ergebnis, egal ob Erfolg oder Fehler, Promise.allSettled() wartet konsequent auf alle Promises und liefert eine vollstaendige Bilanz.

Promise.any() schliesst die verbleibende Luecke: 'Gib mir das erste erfolgreiche Ergebnis, ignoriere einzelne Fehlschlaege, solange mindestens eine Promise erfolgreich ist.' Das trifft genau das Szenario mehrerer redundanter Datenquellen, etwa mehrerer CDN-Spiegel derselben Datei, bei dem ein einzelner langsamer oder fehlerhafter Server die Gesamtantwort nicht verzoegern soll.

2. Promise.any(): Grundlagen

Promise.any(iterable) nimmt ein iterierbares Objekt von Promises entgegen und liefert selbst eine Promise zurueck, die sich erfuellt, sobald die erste uebergebene Promise erfuellt wird, mit exakt deren Wert. Alle anderen, noch laufenden Promises werden nicht abgebrochen, ihre spaeteren Ergebnisse werden schlicht ignoriert, es sei denn, man kuemmert sich explizit um deren Abbruch.

Ein typisches Beispiel: Drei Spiegel-Server bieten dieselbe Konfigurationsdatei an. Statt sequenziell einen nach dem anderen abzufragen oder blind den ersten in der Liste zu nehmen, fragt man alle drei parallel per fetch() ab und verwendet Promise.any(), um automatisch die schnellste erfolgreiche Antwort zu erhalten, unabhaengig davon, welcher Server sie liefert.


const mirrors = [
  'https://mirror1.example.com/config.json',
  'https://mirror2.example.com/config.json',
  'https://mirror3.example.com/config.json',
];

const response = await Promise.any(mirrors.map((url) => fetch(url)));
const config = await response.json();

3. Promise.race() vs. Promise.any()

Der zentrale Unterschied: Promise.race() entscheidet sich fuer das zeitlich erste Ergebnis, unabhaengig davon, ob es sich um Erfolg oder Fehler handelt. Schlaegt der schnellste von drei Servern zuerst fehl, obwohl die anderen beiden kurz danach erfolgreich antworten wuerden, gibt Promise.race() sofort den Fehler weiter, die erfolgreichen Antworten kommen zu spaet, um noch etwas zu aendern.

Promise.any() dagegen ignoriert einzelne Fehlschlaege bewusst und wartet weiter, solange noch mindestens eine Promise offen ist. Erst wenn wirklich alle uebergebenen Promises fehlschlagen, lehnt auch Promise.any() ab. Fuer redundante, fehlertolerante Szenarien wie Spiegel-Server ist das fast immer das gewuenschte Verhalten, waehrend race() eher fuer echte Zeitlimits mit einer einzelnen erwarteten Quelle passt.


// race(): erstes Ergebnis gewinnt, auch ein Fehler
const raceResult = await Promise.race([
  fetch('https://slow-but-reliable.example.com'),
  fetch('https://fast-but-broken.example.com'), // schlaegt zuerst fehl
]); // wirft den Fehler, obwohl der zuverlaessige Server bald geantwortet haette

// any(): Fehler werden ignoriert, solange eine Alternative existiert
const anyResult = await Promise.any([
  fetch('https://fast-but-broken.example.com'),
  fetch('https://slow-but-reliable.example.com'),
]); // liefert die Antwort des zuverlaessigen Servers

4. Unterschied zu Promise.allSettled()

Promise.allSettled() verfolgt ein anderes Ziel als Promise.any(): Es wartet konsequent auf alle Promises, unabhaengig von Erfolg oder Fehlschlag, und liefert am Ende ein Array mit dem Status jeder einzelnen Promise ('fulfilled' oder 'rejected') samt Wert oder Grund. Es gibt also keine Abkuerzung, das letzte Ergebnis bestimmt, wann die zusammengesetzte Promise sich erfuellt.

Der Anwendungsfall unterscheidet sich entsprechend: allSettled() passt, wenn man eine vollstaendige Uebersicht ueber alle Ergebnisse braucht, etwa fuer ein Dashboard, das den Status mehrerer unabhaengiger Health-Checks anzeigen soll. any() passt, wenn nur ein einziges erfolgreiches Ergebnis zaehlt und die uebrigen Promises fuer die eigentliche Aufgabe irrelevant sind, sobald eine davon erfolgreich war. Wer beide Kombinatoren verwechselt, produziert entweder unnoetig lange Wartezeiten (any() statt allSettled() bei einer Statusuebersicht) oder verschenkt die Ausfalltoleranz, die eigentlich der ganze Sinn der Redundanz war.

5. AggregateError korrekt behandeln

Schlagen wirklich alle uebergebenen Promises fehl, lehnt Promise.any() nicht mit einem einzelnen Fehler ab, sondern mit einer Instanz von AggregateError, einem seit ES2021 verfuegbaren Fehlertyp, der mehrere zugrunde liegende Fehler buendelt. Das .errors-Property ist ein Array, das die einzelnen Ablehnungsgruende in derselben Reihenfolge enthaelt wie die urspruenglichen Promises im Iterable.

Wer nur pauschal auf einen Fehlschlag reagieren will, kann AggregateError wie jeden anderen Error in einem catch-Block behandeln. Wer dagegen wissen will, warum konkret jede einzelne Quelle fehlgeschlagen ist, etwa fuer Logging oder Diagnose, iteriert ueber error.errors und wertet jede einzelne Ursache separat aus, statt sich mit einer generischen Fehlermeldung zu begnuegen.


try {
  await Promise.any(mirrors.map((url) => fetch(url)));
} catch (error) {
  if (error instanceof AggregateError) {
    console.error('Alle Spiegel-Server sind fehlgeschlagen:');
    error.errors.forEach((err, i) => console.error(`Server ${i}:`, err.message));
  }
}

6. Praxisbeispiel: Schnellste API-Antwort mit Abbruch der Verlierer

In produktivem Code lohnt es sich, die 'verlierenden' Requests nicht einfach im Hintergrund weiterlaufen zu lassen, sondern aktiv abzubrechen, sobald ein Ergebnis feststeht, um Netzwerk- und Serverlast zu sparen. Dafuer kombiniert man Promise.any() mit AbortController: Jeder Request erhaelt sein eigenes Abort-Signal, und sobald einer erfolgreich ist, werden die uebrigen Controller manuell abgebrochen.

Diese Kombination ist besonders bei mobilen Clients relevant, wo unnoetig weiterlaufende Requests Datenvolumen und Akku kosten. Der Trade-off: Der zusaetzliche Code fuer die Controller-Verwaltung erhoeht die Komplexitaet spuerbar, weshalb sich der Aufwand vor allem bei wirklich teuren oder haeufig ausgefuehrten Redundanz-Requests lohnt, nicht bei jedem beliebigen any()-Aufruf.


async function fetchFastest(urls) {
  const controllers = urls.map(() => new AbortController());
  const requests = urls.map((url, i) =>
    fetch(url, { signal: controllers[i].signal })
  );
  const winner = await Promise.any(requests);
  controllers.forEach((c) => c.abort()); // Verlierer abbrechen
  return winner;
}

7. Kombination mit einem Timeout

Promise.any() hat von Haus aus keinen eingebauten Timeout, es wartet theoretisch unbegrenzt, solange mindestens eine Promise noch offen ist. Sind alle Quellen langsam statt fehlerhaft, kann das zu unerwuenscht langen Wartezeiten fuehren. Die Loesung ist, dem any()-Aufruf selbst per Promise.race() ein Zeitlimit hinzuzufuegen, das bei Ueberschreitung einen eigenen Fehler wirft.

Dabei ist wichtig, dass der Timeout selbst als rejecting Promise formuliert wird, nicht als weiteres Element im any()-Array, sonst wuerde any() den Timeout-Fehler wie jeden anderen Fehlschlag einfach ignorieren, solange eine echte Quelle noch offen ist. Der Timeout muss also auf der Ebene ueber Promise.any() ansetzen, per race() zwischen dem any()-Ergebnis und einem verzoegerten Reject.


function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([promise, timeout]);
}

const result = await withTimeout(Promise.any(mirrors.map((u) => fetch(u))), 3000);

8. Edge Cases: Leeres Array und einzelne Promise

Ein oft uebersehener Sonderfall: Promise.any([]) mit einem leeren Iterable lehnt sofort und synchron mit einem AggregateError ab, dessen .errors-Array leer ist, da es logisch keine erste erfolgreiche Promise geben kann, wenn gar keine Promise existiert. Code, der Listen dynamisch aus Nutzereingaben oder Konfiguration aufbaut, sollte diesen Fall vor dem Aufruf abfangen oder explizit behandeln.

Wird nur eine einzelne Promise uebergeben, verhaelt sich Promise.any() im Erfolgsfall wie ein einfaches await, im Fehlerfall aber weicht es ab: Statt des urspruenglichen Fehlers direkt zu werfen, verpackt es ihn in ein AggregateError mit einem einzelnen Eintrag im .errors-Array, was Fehlerbehandlungscode beruecksichtigen muss, der zwischen ein und mehreren Quellen generisch umgehen soll.


try {
  await Promise.any([]);
} catch (error) {
  console.log(error instanceof AggregateError); // true
  console.log(error.errors); // []
}

9. Best Practices und Zusammenfassung

Promise.any() ist der richtige Kombinator immer dann, wenn mehrere redundante Quellen dasselbe Ergebnis liefern koennen und nur die schnellste erfolgreiche Antwort zaehlt, waehrend einzelne Fehlschlaege toleriert werden sollen. Die Abgrenzung zu race() (reagiert auch auf Fehler) und allSettled() (wartet immer auf alle) ist dabei der wichtigste konzeptionelle Unterschied.

In produktivem Code gehoert dazu fast immer eine explizite AggregateError-Behandlung fuer den Fall, dass wirklich alle Quellen fehlschlagen, sowie je nach Szenario ein zusaetzlicher Timeout-Wrapper und aktives Abbrechen der Verlierer-Requests per AbortController, um Ressourcen zu sparen, statt sie einfach im Hintergrund verpuffen zu lassen.

Kombinator Erfuellt sich bei Lehnt ab bei Typischer Einsatz
Promise.all() Alle erfolgreich Erstem Fehler Abhaengige parallele Tasks
Promise.race() Erstem beliebigen Ergebnis Erstem beliebigen Fehler Echte Zeitlimits
Promise.any() Erstem Erfolg Wenn alle fehlschlagen (AggregateError) Redundante Spiegel-Quellen
Promise.allSettled() Immer (nach allen) Nie Vollstaendige Status-Uebersicht

Mironsoft

Moderne Browser-APIs, Performance und wartbares JavaScript

JavaScript, das im echten Browser robust bleibt, nicht nur im Tutorial?

Wir prüfen bestehenden Frontend-Code auf veraltete Patterns, unnötige Bibliotheken und Performance-Fallen und ersetzen sie durch moderne, native Browser-APIs, die weniger Bundle-Gewicht und weniger Wartungslast bedeuten.

Code-Review

Veraltete Patterns, unnötige Dependencies und Memory Leaks systematisch aufspüren.

Performance-Optimierung

Bundle-Größe, Ladezeit und Runtime-Performance mit modernen APIs verbessern.

Modernisierung

Native Browser-APIs statt schwerer Bibliotheken gezielt einführen.

10. Zusammenfassung

Promise.any(): Das Wichtigste auf einen Blick

Kernverhalten

Promise.any() erfuellt sich mit dem ersten Erfolg und ignoriert einzelne Fehlschlaege, solange eine Alternative offen ist.

Abgrenzung zu race()

race() reagiert auch auf den ersten Fehler, any() wartet dagegen konsequent auf einen Erfolg.

AggregateError

Schlagen alle Promises fehl, lehnt any() mit AggregateError ab, dessen .errors-Array alle Einzelfehler enthaelt.

Kein eingebauter Timeout

any() wartet unbegrenzt, ein Zeitlimit muss per Promise.race() um den any()-Aufruf herum ergaenzt werden.

11. FAQ: Promise.any(): Das Wichtigste auf einen Blick

1Wann sollte ich Promise.any() statt Promise.race() verwenden?
Immer wenn einzelne Fehlschlaege toleriert werden sollen und nur das erste erfolgreiche Ergebnis zaehlt, etwa bei redundanten Spiegel-Servern. race() eignet sich eher fuer echte Zeitlimits mit einer erwarteten Quelle.
2Was passiert, wenn alle Promises bei any() fehlschlagen?
Die zusammengesetzte Promise lehnt mit einem AggregateError ab, dessen .errors-Array alle einzelnen Ablehnungsgruende in der urspruenglichen Reihenfolge enthaelt.
3Bricht Promise.any() automatisch die verbleibenden Promises ab?
Nein, alle uebrigen Promises laufen unveraendert weiter, ihre Ergebnisse werden nur ignoriert. Fuer aktives Abbrechen braucht es zusaetzlich AbortController pro Request.
4Gibt es einen eingebauten Timeout bei Promise.any()?
Nein, any() wartet theoretisch unbegrenzt. Ein Zeitlimit laesst sich per Promise.race() zwischen dem any()-Aufruf und einem verzoegerten Reject ergaenzen.
5Was liefert Promise.any([]) mit einem leeren Array?
Es lehnt sofort und synchron mit einem AggregateError ab, dessen .errors-Array leer ist, da es keine Promise gibt, die erfolgreich sein koennte.
6Wie unterscheidet sich any() von allSettled()?
allSettled() wartet immer auf alle Promises und liefert eine vollstaendige Status-Uebersicht, any() liefert dagegen nur das erste erfolgreiche Ergebnis und ignoriert die uebrigen.
7Ist AggregateError ein normaler Error?
Es erbt von Error und laesst sich wie jeder andere Fehler in try/catch behandeln, bietet zusaetzlich aber das .errors-Array fuer die einzelnen zugrunde liegenden Fehlschlaege.
8Was passiert bei genau einer uebergebenen Promise?
Bei Erfolg verhaelt sich any() wie ein normales await, bei Fehlschlag wird der urspruengliche Fehler dennoch in ein AggregateError mit einem Eintrag verpackt, nicht direkt geworfen.
9Eignet sich Promise.any() fuer sequenzielle Fallback-Ketten?
Nur bedingt, da alle Promises parallel starten. Fuer echte Reihenfolge mit Fallback erst bei Fehlschlag ist eine sequenzielle await-Kette oder ein eigener Retry-Mechanismus passender.
10Seit wann ist Promise.any() verfuegbar?
Seit ES2021, unterstuetzt in allen aktuellen evergreen Browsern und in Node.js ab Version 15, aeltere Umgebungen benoetigen einen Polyfill oder core-js.