Die async/await-Syntax von JavaScript sollte uns vor der Callback Hell bewahren. Stattdessen hat sie ein leiseres, heimtückischeres Problem eingeführt: Code, der korrekt aussieht, sich aber unvorhersehbar verhält. Man sieht das await im Funktionskörper und nimmt an, dass alles höflich Zeile für Zeile pausiert. Oft ist das nicht der Fall. Schleifen rasen voran. Ganze Batches brechen wegen einer einzigen fehlgeschlagenen Anfrage zusammen. Entry-Files entwickeln ohne Grund hässliche async-Wrapper. Wenn Sie auf eines dieser Probleme gestoßen sind, werden diese drei Muster sie lösen.

Hören Sie auf, await innerhalb von forEach zu verwenden

Hier ist ein häufiger Fehler, der auf den ersten Blick harmlos aussieht:

const urls = ['/api/user', '/api/posts', '/api/comments'];

urls.forEach(async (url) => {
  const res = await fetch(url);
  const data = await res.json();
  console.log(data);
});

console.log('All done!');

Wenn Sie dies ausführen, wird 'All done!' ausgegeben, noch bevor eine einzige Antwort zurückkommt. Warum? forEach führt den Callback für jedes Element sofort aus. Es wartet nicht auf das Promise innerhalb jeder Iteration. Das async-Schlüsselwort verwandelt jeden Callback in ein Promise, das forEach kurzerhand ignoriert. Ihre Schleife ist in Mikrosekunden beendet; die Netzwerkanfragen laufen eigenständig davon. Wenn Sie benötigen, dass Fehler in der richtigen Reihenfolge behandelt werden, oder wenn Sie garantieren müssen, dass eine Anfrage abgeschlossen ist, bevor die nächste beginnt, bricht dieses Muster beide Garantien stillschweigend.

Ersetzen Sie es durch eine for...of-Schleife:

const urls = ['/api/user', '/api/posts', '/api/comments'];

for (const url of urls) {
  const res = await fetch(url);
  const data = await res.json();
  console.log(data);
}

console.log('All done!');

Jetzt hält die Schleife bei jedem await tatsächlich an. Die zweite Anfrage wartet auf die erste. 'All done!' wird erst ausgegeben, wenn alles abgeschlossen ist.

Verwenden Sie for...of, wenn die Reihenfolge wichtig ist – zum Beispiel beim Hochladen von Dateien nacheinander, um Rate Limits einzuhalten, beim Schreiben von Datenbankzeilen in einer bestimmten Reihenfolge oder beim Verketten von API-Aufrufen, bei denen die nächste Anfrage Daten aus der vorherigen Antwort benötigt. Wenn Sie tatsächlich eine parallele Ausführung wünschen, experimentieren Sie nicht mit forEach. Greifen Sie explizit zu Promise.all, damit Ihre Absicht für den nächsten Entwickler ersichtlich ist. Aber mischen Sie niemals await und forEach in der Erwartung eines synchronen Verhaltens. Das wird nicht passieren.

Greifen Sie zu Promise.allSettled, wenn Null nicht die Antwort sein kann

Promise.all ist semantisch ehrlich. Man übergibt ein Array von Promises, und es gibt ein Array von Ergebnissen zurück. Der Haken ist: In dem Moment, in dem ein einziges Promise abgelehnt wird (reject), wird das gesamte Konstrukt sofort abgelehnt. Jedes andere ausstehende Promise läuft zwar weiter, aber Sie verlieren den Zugriff auf deren Ergebnisse. In der Produktion ist dieses Alles-oder-Nichts-Verhalten schmerzhaft.

Stellen Sie sich vor, Ihre Anwendung ruft die Widgets eines Dashboards von vier unabhängigen Diensten ab: Traffic-Analyse, Umsatzdaten, Nutzerfeedback und Serverstatus. Die Umsatz-API läuft in einen kurzen Timeout. Bei Verwendung von Promise.all wirft Ihr gesamtes Dashboard einen Fehler. Die drei gesunden Antworten verschwinden im Nichts. Der Benutzer sieht einen Ladekreis und dann eine Fehlermeldung, nur weil ein Viertel der Daten nicht korrekt funktioniert hat.

Promise.allSettled bietet Ihnen einen vernünftigeren Vertrag. Es wartet, bis jedes einzelne Promise abgeschlossen ist, egal wie das Ergebnis ausfällt. Der aufgelöste Wert ist ein Array von Objekten, die jedes Ergebnis beschreiben:

const requests = [
  fetch('/api/traffic'),
  fetch('/api/revenue'),
  fetch('/api/feedback'),
  fetch('/api/health')
];

const results = await Promise.allSettled(requests);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    renderWidget(index, result.value);
  } else {
    renderError(index, result.reason);
  }
});

Keine Antwort wird verworfen. Sie rendern das, was möglich ist, und isolieren den Fehler. Dieses Muster ist wichtig, wann immer Sie mit nicht zusammenhängenden Operationen zu tun haben – Massennachrichten, Webhook-Dispatches von Drittanbietern oder das Importieren von Datensätzen aus mehreren CSV-Streams. Sie benötigen zwar weiterhin ein zentralisiertes Error-Tracking, aber Ihre Anwendung bleibt stabil.

Ein praktischer Hinweis: allSettled gibt den vollständigen Satz zurück, sodass Sie die Ergebnisse immer noch durchforsten und entscheiden müssen, was „Teilerfolg“ für Ihr Feature bedeutet. Behandeln Sie das zurückgegebene Array nicht als durchgehend positive Daten. Überprüfen Sie die Statusfelder, bevor Sie etwas in Ihren State-Layer pushen.

Deklarieren Sie Top-Level await und eliminieren Sie den Wrapper-IIFE

Jahrelang musste man, wenn man am Anfang einer Datei auf etwas warten wollte, dies in eine sofort aufgerufene asynchrone Funktion (IIFE) einbetten:

(async () => {
  const config = await loadConfig();
  startServer(config);
})();

Das funktioniert zwar, ist aber unnötiger Ballast. Top-Level await, nativ in ES-Modulen verfügbar, erlaubt es Ihnen, diesen zeremoniellen Boilerplate-Code wegzulassen:

const config = await loadConfig();
startServer(config);

Verwenden Sie dies am Einstiegspunkt Ihrer Anwendung oder in dedizierten Konfigurationsmodulen, in denen die Initialisierung abgeschlossen sein muss, bevor etwas anderes ausgeführt wird. Das Laden von Environment-Dateien, der Aufbau eines Datenbank-Connection-Pools oder das Abrufen von Remote-Feature-Flags sind dafür ideale Anwendungsfälle. Da Top-Level await die Ausführung des Modul-Graphen blockiert – andere Dateien, die dieses Modul importieren, warten darauf, dass Ihr Promise aufgelöst wird –, erhalten Sie einen garantierten Zustand. Der Rest Ihres Codes kann db importieren und sicher sein, dass die Verbindung bereits steht.

Es gibt zwei Haken. Erstens muss Ihre Laufzeitumgebung oder Ihr Bundler ES-Module unterstützen. In Node.js bedeutet das entweder die Verwendung der Dateiendung .mjs oder das Setzen von "type": "module" in Ihrer package.json. Zweitens: Da das Warten auf Modulebene jeden Importeur verzögert, sollten Sie die zu wartenden Aufgaben fokussiert halten. Schwere sequentielle Abrufe am Anfang einer häufig importierten Utility-Datei werden den Cold Start Ihrer gesamten Anwendung verlangsamen. Reservieren Sie Top-Level-Await für echte Bootstrap-Aufgaben, von denen andere Module tatsächlich abhängen.

Was sich tatsächlich ändert, wenn Sie diese Muster übernehmen

Vorhersehbarkeit ist der erste Gewinn. Wenn Sie eine for...of-Schleife lesen, wissen Sie genau, wann der darunter liegende Block abgeschlossen ist. Es gibt keine Geister-Promises, die im Hintergrund mitlaufen, keine foreach-Callbacks, die sich von Ihren Error-Handlern lösen. Ihr Kontrollfluss entspricht der Struktur des Codes auf dem Bildschirm.

Resilienz folgt als Nächstes. Promise.allSettled zwingt Sie dazu, über Teilausfälle nachzudenken, anstatt zu hoffen, dass jedes externe System perfekt funktioniert. Produktionssoftware ist nicht binär. Einige Endpunkte werden unzuverlässig sein. Einige Dateilesen werden auf Berechtigungsfehler stoßen. Das Design für die Realität sporadischer Fehler hält Ihre Anwendung stabil, ohne legitime Daten unter den Teppich zu kehren.

Klarheit rundet das Ganze ab. for...of liest sich wie eine natürliche, logische Abfolge. allSettled drückt seine Absicht bereits im Namen aus. Top-Level-Await entfernt kryptische IIFE-Wrapper, sodass Ihre Entry-Files mit der Geschäftslogik beginnen, anstatt mit syntaktischen Akrobatiken. Der nächste Ingenieur, der die Datei anfasst – sei es Sie selbst in sechs Monaten oder ein Teamkollege unter Zeitdruck – wird es Ihnen danken.

Ein wichtiges Fazit

Behandeln Sie async/await nicht als globale Lösung, die Sie einfach über bestehenden Code streuen. Prüfen Sie Ihre aktuellen Projekte auf diese drei spezifischen Anti-Patterns. Suchen Sie nach await innerhalb von forEach-Blöcken und ersetzen Sie diese durch for...of oder ein gezieltes Promise.all. Überprüfen Sie jedes Promise.all, das mit externen Diensten kommuniziert, und fragen Sie sich, ob ein einzelner Fehler wirklich die gesamte Operation torpedieren sollte; wenn nicht, wechseln Sie zu Promise.allSettled und verarbeiten Sie die gemischten Ergebnisse. Entfernen Sie schließlich die async IIFEs aus Ihren ES-Modul-Entry-Points und lassen Sie Top-Level-Await Ihre Bootstrap-Sequenz direkt verwalten. Dies sind kleine mechanische Änderungen, aber zusammen verwandeln sie fragile asynchrone Skripte in Code, dem Sie tatsächlich vertrauen können.