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().
Inhaltsverzeichnis
- 1. Vier Promise-Kombinatoren, vier Szenarien
- 2. Promise.any(): Grundlagen
- 3. Promise.race() vs. Promise.any()
- 4. Unterschied zu Promise.allSettled()
- 5. AggregateError korrekt behandeln
- 6. Praxisbeispiel: Schnellste API-Antwort mit Abbruch der Verlierer
- 7. Kombination mit einem Timeout
- 8. Edge Cases: Leeres Array und einzelne Promise
- 9. Best Practices und Zusammenfassung
- 10. Zusammenfassung
- 11. FAQ
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.