Startseite / Artikel / Neun Versprechenmuster für zuverlässiges Asynchron-JavaScript in der Produktion

Neun Versprechenmuster für zuverlässiges Asynchron-JavaScript in der Produktion

Erlernen Sie praktische Promise-Muster – parallele Anfragen, Zeitlimits, Wiederholungsversuche, Konkurrenzbeschränkungen sowie Stornierung – zur Erstellung widerstandsfähiger, produktionsreifer asynchroner JavaScript-Anwendungen.

1998 Wörter

Ihr greift ständig auf Promises zurück, doch einige weniger bekannte Muster können verworrenen asynchronen Code in etwas Vorhersehbares und Verständliches verwandeln.

Die meisten Menschen lernen Promises durch ein Beispiel wie dieses:

fetch("/api/users")
.then((res) => res.json())
.then((users) => console.log(users));

Und für einfache Skripte ist das tatsächlich alles, was man braucht.

Probleme entstehen erst, wenn man zu produktionsreifen Anwendungen übergeht.

Man braucht eine Möglichkeit, Arbeiten zu stornieren, die nicht mehr relevant sind.

Man braucht Wiederholungsversuche, wenn eine Anfrage fehlschlägt.

Man muss weitermachen, selbst wenn nur ein Teil einer Operation erfolgreich ist.

Man muss verhindern, dass derselbe API-Aufruf versehentlich zweimal ausgeführt wird.

Manchmal muss man außerdem begrenzen, wie viele asynchrone Operationen gleichzeitig laufen, damit der Backend nicht mit allem auf einmal überlastet wird.

Dies ist der Punkt, an dem Promises nicht mehr ein Anfängertopik sind, sondern zu einem echten Designwerkzeug werden.

Unten finden Sie neun Muster, die in Ihrem Werkzeugkasten beim Schreiben moderner JavaScript-Code nützlich sind.

1. Unabhängige Anfragen parallel ausführen

Einer der einfachsten Vorteile von asynchronem Code ist zugleich einer der leichtesten zu übersehenden.

Nehmen wir an, Ihre Seite benötigt drei Dinge: Benutzerdaten, Benachrichtigungen und Analyse-Daten.

Ein gängiger erster Ansatz sieht so aus:

const users = await getUsers();
const notifications = await getNotifications();
const analytics = await getAnalytics();
Each request waits for the previous one.

Jeder await blockiert, bis der vorherige Aufruf abgeschlossen ist, sodass die Aufrufe nacheinander abgearbeitet werden.

Nehmen wir an, jeder Aufruf dauert etwa 500 Millisekunden – dann könnte diese sequenzielle Abfolge insgesamt etwa 1,5 Sekunden Wartezeit erfordern.

Falls keiner dieser Aufrufe tatsächlich voneinander abhängt, gibt es keinen Grund zu warten.

Dafür ist genau Promise.all() da:

const [users, notifications, analytics] = await Promise.all([
getUsers(),
getNotifications(),
getAnalytics(),
]);

Sie alle drei Anfragen werden nun gleichzeitig abgesendet anstelle dessen, dass sie nacheinander erfolgen.

Für Dashboards oder jede Anzeige, die mehrere unabhängige Datenquellen lädt, kann dies die Ladezeit erheblich verkürzen.

Die wichtige Einschränkung

Promise.all() versagt schnell: Sobald eine der Promises abgelehnt wird, wird die gesamte Gruppe zusammen mit ihr abgelehnt.

Das ist in Ordnung, wenn jede Anfrage zwingend erforderlich ist, doch wenn einige der Daten optional sind, benötigt man einen nachsichtigeren Ansatz.

2. Verwenden Sie Promise.allSettled(), wenn teilweise fehlgeschlagene Anfragen in Ordnung sind

Stellen Sie sich ein Admin-Dashboard vor, das Folgendes anzeigt:

  • Einnahmen
  • Benutzer
  • Benachrichtigungen
  • Systemzustand

Falls der Dienst für Benachrichtigungen zufällig nicht verfügbar ist, sollte dann die gesamte Anzeige leer werden?

In der Regel nicht – das Fehlen eines Bereichs sollte den Rest der Seite nicht beeinträchtigen.

Promise.allSettled() löst genau dieses Problem.

const results = await Promise.allSettled([
getRevenue(),
getUsers(),
getNotifications(),
getSystemHealth(),
]);
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.error("Failed:", result.reason);
}
});

Eine einzige fehlgeschlagene Aufrufung löscht nicht mehr die daneben liegenden erfolgreichen Ergebnisse aus.

Dieses Muster ist auf Dashboards, Analysebildschirmen und Überwachungstools nützlich, wo das Anzeigen von Teildaten besser ist als gar nichts anzuzeigen.

3. Ein Timeout zu einer Promise hinzufügen

Schneller oder später stoßen Sie auf dieses Szenario: Was passiert, wenn eine Anfrage einfach nie zurückkommt?

Ohne Schutzmaßnahme kann Ihre Benutzeroberfläche endlos hängen bleiben.

Man kann einen kleinen, wiederverwendbaren Wrapper erstellen, der einen Timeout vorschreibt:

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

Dann verwenden Sie ihn überall dort, wo Sie eine Anfrage senden:

try {
const data = await withTimeout(fetch("/api/data"), 5000);
console.log(data);
} catch (error) {
console.error(error);
}

Falls der Aufruf innerhalb von fünf Sekunden nicht abgeschlossen ist, lehnt die umhüllte Promise von selbst ab.

Das ist eine weitaus bessere Erfahrung als dass die Benutzer weiterhin auf einen nie abgeschlossenen Ladeindikator starren müssen.

4. Versuche fehlgeschlagener Operationen erneut

Netzwerke unterbrechen die Verbindungen.

Server funktionieren zeitweise nicht richtig.

Drittanbieter-APIs verhalten sich manchmal unerwartet.

Das bedeutet nicht zwingend, dass der Benutzer sofort eine Fehlermeldung sehen muss.

Für vorübergehende Ausfälle hilft ein einfacher Erneutversuch-Helfer sehr weit.

async function retry(fn, attempts = 3) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
}
}
throw lastError;
}

Man ruft es so auf:

const data = await retry(
() => fetch("/api/data"),
3
);

Die Operation erhält nun einige zusätzliche Versuche, bevor sie als echter Fehler behandelt wird.

Aber versuchen Sie nicht blind alles erneut.

Ein 500-Status signalisiert oft ein vorübergehendes Serverproblem.

Ein 401 Unauthorized-Status hingegen lässt sich nicht durch drei weitere Anfragen beheben.

Eine solide Erneutversuch-Logik unterscheidet zwischen Fehlern, die es wert sind, erneut versucht zu werden, und solchen, die das nicht tun.

5. Pausen zwischen den Erneutversuchen einbauen

Es ist nicht immer die klügste Entscheidung, sofort bei einem fehlgeschlagenen Antrag erneut zu versuchen.

Betrachten Sie einen Server, der bereits unter Last leidet.

Falls Tausende von Clients zur gleichen Zeit erneut versuchen, übt das zusätzlichen Druck auf ein ohnehin schon überlastetes System aus.

Eine einfache Verzögerungsfunktion löst dieses Problem:

function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

Man kann sie direkt in die Wiederholungsloipe einfügen:

async function retry(fn, attempts = 3) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i < attempts - 1) {
await delay(1000);
}}}
throw lastError;
}

Produktivsysteme gehen oft einen Schritt weiter und verwenden exponentielles Backoff, wodurch die Wiederholungsversuche so gestaffelt werden:

1 second
2 seconds
4 seconds
8 seconds

Durch diese Gestaffelung erhält der überlastete Server Zeit zur Erholung, anstatt eine Wiederholungssturm auszulösen.

6. Konkurrenz steuern

Promise.all() kann ein Problem verursachen, das unbemerkt bleibt.

Nehmen wir an, Sie müssen 1.000 API-Anfragen verarbeiten.

Der naive Ansatz scheint harmlos genug zu sein:

await Promise.all(
items.map((item) => processItem(item))
);
But now you’re potentially starting 1,000 operations at once.

Aber das bedeutet, dass Sie alle 1.000 Operationen genau zum gleichen Zeitpunkt ausführen könnten.

Das ist selten das, was Sie tatsächlich wollen.

Oft möchten Sie lieber eine Obergrenze für die Anzahl der parallel ausgeführten Operationen festlegen.

Bilden Sie sich etwas wie folgt vor:

1000 tasks
↓
5 at a time
↓
5 at a time
↓
5 at a time

Einen einfachen Konkurrenzbeschränker zu erstellen ist nicht schwierig:

async function runWithLimit(items, limit, fn) {
const results = [];
let index = 0;
async function worker() {
while (index < items.length) {
const currentIndex = index++;
results[currentIndex] = await fn(items[currentIndex]);
}
}
const workers = Array.from(
{ length: limit },
() => worker()
);
await Promise.all(workers);
return results;
}

Man verwendet ihn so:

const results = await runWithLimit(
items,
5,
(item) => processItem(item)
);

Nun entscheiden Sie selbst genau, wie viele Aufgaben gleichzeitig ausgeführt werden, anstatt es dem Laufzeitumfeld zu überlassen.

Diese Technik bringt enorme Vorteile, wenn Sie mit großen Datensätzen, externen APIs, Dateiverarbeitung oder Hintergrundjob-Queues umgehen.

7. Doppelte Anfragen verhindern

Hier ist ein Problem, das häufiger auftritt, als man erwarten würde.

Ein Benutzer lädt eine Dashboard-Seite hoch.

Drei separate Komponenten benötigen jeweils die gleichen Benutzerprofildaten.

Anstatt drei separate Aufrufe zu senden:

Component A → /api/user
Component B → /api/user
Component C → /api/user

können die Komponenten stattdessen einen einzigen Promise teilen.

const pendingRequests = new Map();
function getUser(id) {
if (pendingRequests.has(id)) {
return pendingRequests.get(id);
}
const request = fetch(`/api/users/${id}`)
.then((res) => res.json())
.finally(() => {
pendingRequests.delete(id);
});
pendingRequests.set(id, request);
return request;
}

Durch diese Konfiguration werden, wenn drei Komponenten ungefähr zur gleichen Zeit nach demselben Benutzer fragen, alle an einer einzigen laufenden Anfrage gebunden, anstatt drei neue Anfragen ausgelöst zu werden.

Anders ausgedrückt:

3 Anfragen → 1 Anfrage

Diese Technik wird oft als Anfragededuplizierung bezeichnet.

In größeren Codebasen implementieren Tools wie TanStack Query bereits eine solche Caching- und Deduplizierungslogik für Sie.

8. Verwenden Sie Promise.any(), wenn nur ein erfolgreiches Ergebnis benötigt wird

Mannchmal ist derselbe Datensatz an mehreren verschiedenen Stellen verfügbar.

Zum Beispiel:

API Server A
API Server B
API Server C

Falls Ihre Anwendung nur eine einzige erfolgreiche Antwort von irgendeiner dieser Quellen benötigt, eignet sich Promise.any() hervorragend dafür.

const result = await Promise.any([
fetchFromServerA(),
fetchFromServerB(),
fetchFromServerC(),
]);

Jeder Promise, der zuerst erfüllt wird, gewinnt den Wettbewerb.

Bemerken Sie, dass sich dies von Promise.race() unterscheidet.

Promise.race() löst sich auf oder lehnt ab, je nachdem, welcher Promise zuerst settles, also entweder erfolgreich oder fehlgeschlagen.

Promise.any() hingegen wartet speziell auf den ersten Promise, der erfolgreich erfüllt wird, und ignoriert Ablehnungen, es sei denn, alle scheitern.

Der Unterschied mag gering erscheinen, kann aber völlig verändern, wie Sie mit Fehlern umgehen.

9. Arbeit, die Sie nicht mehr benötigen, stornieren

Dieses Muster gehört zu den zufriedenstellendsten, die man anwenden kann.

Denken Sie an ein Suchfeld in Echtzeit:

user types: react
user types: react dashboard
user types: react dashboard ui

Wahrscheinlich möchten Sie nicht, dass die Anfragen aus früheren Tastatureingaben weiterhin im Hintergrund ausgeführt werden, sobald sie veraltet sind.

Das ist genau die Situation, für die AbortController entwickelt wurde.

const controller = new AbortController();
fetch("/api/search?q=react", {
signal: controller.signal,
});

Sobald die Anfrage nicht mehr benötigt wird, rufen Sie einfach auf:

controller.abort();
The request can then be cancelled.

Dieses Muster tritt ständig in Szenarien wie folgenden auf:

  • Suchvorschläge
  • Autocomplete-Felder
  • Übergänge zwischen Routen oder Seiten
  • Abschalten/Reinigen von Komponenten
  • Anfragen, die durch neuere ersetzt werden

Die eigentliche Fähigkeit besteht hier nicht nur darin, abort() aufzurufen.

Sondern darin, den Moment zu erkennen, in dem frühere Arbeiten nicht mehr nützlich sind.

Die wichtigere Lektion

Promises erscheinen nicht schwierig, weil .then() eine komplexe API ist.

Sie erscheinen schwierig, weil reale Anwendungen mit unübersichtlichem, mehrschichtigem asynchronen Verhalten zu kämpfen haben.

Man muss ständig Dinge wie folgende berücksichtigen:

Sollten diese Operationen gleichzeitig ausgeführt werden?

Was ist der Plan, falls eine von ihnen fehlschlägt?

Wie lange ist zu lange, um zu warten?

Lohnt es sich hier, es erneut zu versuchen?

Wie viele Operationen sollten gleichzeitig laufen?

Kann ich die Wiederholung bereits im Gange befindlicher Arbeiten vermeiden?

Ist diese Anfrage noch relevant?

Sobald man solche Fragen stellt, sind Promises nicht mehr nur eine Sprachfunktion. Sie werden zu einem echten architektonischen Werkzeug.

M meine 9 Promise-Muster im Überblick

Promise.all() eignet sich am besten zum gleichzeitigen Ausführen unabhängiger Aufgaben. Promise.allSettled() handhabt teilweise fehlgeschlagene Operationen angemessen. Promise.race() ist geeignet für Zeitlimits oder um auf die erste abgeschlossene Operation zu reagieren. Eine Wiederholungslogik hilft dabei, temporäre Fehler zu überwinden. Verzögerungs- und Rücksetzstrategien verhindern, dass die Wiederholungen zu aggressiv werden. Die Begrenzung der Parallelität hält große Arbeitslasten unter Kontrolle. Die Deduplizierung von Anfragen verhindert doppelte API-Aufrufe. Promise.any() liefert das erste erfolgreiche Ergebnis unter mehreren. AbortController ermöglicht es, Arbeiten zu stornieren, die nicht mehr notwendig sind.

Ihnen sind nicht alle diese Muster in jedem Projekt erforderlich.

Tatsächlich wäre es kontraproduktiv, alle davon zu verwenden.

Die eigentliche Fähigkeit besteht darin, vor dem Einsatz eines bestimmten Musters herauszufinden, welches Problem Sie lösen möchten.

Letzter Gedanke

Der stärkste asynchrone Code ist nicht der, der mit den raffiniertesten Promise-Techniken gefüllt ist.

Es ist der Code, bei dem Fehlerbehandlung, Zeitmanagement, Konkurrenz und Abbruch bereits im Voraus durchdacht wurden, bevor sie zu Produktionsproblemen führten.

Fangen Sie mit der einfachsten Version an.

Achten Sie darauf, was tatsächlich Probleme verursacht.

Nur dann sollten Sie das spezifische Muster einsetzen, das dieses Problem löst.

Denn manchmal ist der fortschrittlichste Schritt in JavaScript, zu erkennen wann ein Muster überhaupt nicht benötigt wird.

Zusätzliche Lektüre

  • Die Wahl zwischen Promise.all, Promise.race und sequenziellen Awaits — Erfahren Sie, wann Promise.all() Node.js-APIs beschleunigt, warum es bei jeder Ablehnung schnell versagt, sowie ein Entscheidungsframework zur Auswahl des richtigen asynchronen Musters.
  • Async/Await gegen Promises: Was sich tatsächlich im Hintergrund unterscheidet — Erklärt die wirklichen Unterschiede in Bezug auf Ausführung, Speicherverbrauch und Stack-Trace zwischen async/await und Promises sowie wann man weiterhin auf die reinen Promise-APIs zurückgreifen sollte.