Was async/await tatsächlich garantiert und was es Ihnen überlässt.
Verstehen, was await tatsächlich aussetzt, wie serielle Anfragen vermieden werden können und warum Fehler, Stornierungen, Sortierung sowie Wiederholungsversuche eine über async/await hinausgehende Gestaltung erfordern.
Eine Funktion, die voller await-Ausdrücke ist, liest sich wie ein Skript, das Zeile für Zeile ausgeführt wird – und genau diese Wahrnehmung ist der Ausgangspunkt vieler asynchroner Fehler. Langsame Dashboards, unterdrückte Fehler, veraltete Suchergebnisse sowie doppelt berechnete Zahlungen entstehen oft dadurch, dass man von async/await Erwartungen hat, die es nie erfüllen kann. Sobald man den genauen, im Grunde recht einfachen Ablauf hinter der Syntax kennt, erkennt man auf einen Blick, welches Verhalten vom Sprachkonstrukt selbst stammt und welches man noch selbst entwerfen muss.
Hier ist eine Funktion, die eindeutig sequenziell erscheint:
async function loadDashboard(userId) {
const user = await fetchUser(userId);
const projects = await fetchProjects(userId);
const notifications = await fetchNotifications(userId);
return {
user,
projects,
notifications,
};
}
Die natürliche Lesart lautet: Benutzer abrufen, warten, Projekte abrufen, warten, Benachrichtigungen abrufen und anschließend zurückgeben. Diese Lesart ist nicht ganz falsch, verdeckt aber den wichtigen Aspekt, sobald der Code schnell, koncurrent, stornierbar oder fehlerresistent sein muss.
await sagt nichts darüber, wie die zugrunde liegende Arbeit ausgeführt wird. Es markiert lediglich den Punkt, an dem die umschließende asynchrone Funktion pausiert werden muss, bis eine Promise abgeschlossen ist. Während diese Funktion pausiert ist, setzt die Laufzeit weitere Aufgaben fort. Viele Überraschungen entstehen dadurch, dass Verhaltensweisen, die eigentlich zu Promises, zur Hostumgebung, zu einer API wie fetch, zu einer Konkurrenzstrategie oder zur Anwendung selbst gehören, dem async/await-Konzept zugeschrieben werden.
await pausiert nur eine Funktion, nicht die gesamte Laufzeit
Betrachten Sie dieses kleine Programm, das um einen Netzwerkaufruf herum Protokolle anlegt:
async function loadUser() {
console.log("A");
const user = await fetch("/api/user");
console.log("B");
return user;
}
console.log("1");
loadUser();
console.log("2");
Durch Aufruf von loadUser() wird sein Codekörper sofort ausgeführt, weshalb zuerst „A“ und anschließend „1“ protokolliert werden. Wenn die Ausführung auf das await stößt, blockiert die Funktion den Thread nicht, solange der Anfrage abgewickelt wird. Stattdessen pausiert sie sich selbst und gibt die Kontrolle an den Aufrufer zurück, wodurch „2“ vor „B“ erscheint. Sobald die fetch-Promise abgeschlossen ist, wird der Rest von loadUser() zur Fortsetzung in die Warteschlange gelegt, und erst dann wird „B“ protokolliert. Die resultierende Reihenfolge ist somit 1, A, 2, B.
Daher lautet die korrekte Übersetzung von await nicht „Stoppen des JavaScript-Code hier“. Sie ist eher „Alles nach dieser Zeile hängt von diesem Wert ab, daher wird diese Funktion später fortgesetzt“.
Auch dann gilt das, wenn der erwartete Wert bereits verfügbar ist. Das Warten auf eine erfüllte Promise oder einen einfachen, nicht-promise-basierten Wert verschiebt den Rest der Funktion weiterhin auf einen späteren Microtask, anstatt sie direkt fortzusetzen – wie in den MDN-Dokumentationen beschrieben. Deshalb kann das Hinzufügen scheinbar harmloser zusätzlicher await-Ausdrücke die Scheduling-Strategie verändern: Jeder solche Ausdruck fügt eine weitere Microtask-Grenze hinzu.
Die praktische Folge ist, dass man ein Programm nicht verstehen kann, wenn man jedes await als blockierende Aufruf in einer synchronen Funktion betrachtet. Man muss nachverfolgen, was bereits gestartet wurde, welche Funktionen derzeit pausiert sind und was sonst ausgeführt werden darf, bevor eine bestimmte Funktion wieder fortgesetzt wird.
async verschiebt die Arbeit nicht vom Hauptthread
Das Schlüsselwort async führt zu einem weiteren Missverständnis. Schauen Sie sich diesen CPU-intensiven Loop an:
async function calculateTotal(items) {
let total = 0;
for (const item of items) {
total += expensiveCalculation(item);
}
return total;
}
Es gibt hier nichts Konkurrenzfähiges. Das Hinzufügen von async bringt expensiveCalculation() nicht auf einen anderen Thread, parallelisiert den Loop nicht und verhindert auch nicht, dass aufwendige synchrone Aufgaben alles blockieren, was sonst auf demselben Thread ausgeführt werden soll.
Was async verändert, ist das Rückgabeverhalten der Funktion. Der Aufruf liefert stets ein Promise, und das Zurückgeben eines gewöhnlichen Wertes erfüllt dieses Promise mit diesem Wert. Die ECMAScript-Spezifikation beschreibt die Auswertung von async-Funktionen im Hinblick auf eine Promise-Fähigkeit, die zum Ergebnis der Funktion wird.
async function getNumber() {
return 42;
}
const result = getNumber();
console.log(result);
// Promise
Das Protokollieren von result zeigt ein ausstehendes oder erfülltes Promise, nicht 42. Den Wert erhält man nur, indem man auf das Promise wartet oder .then() darauf aufruft.
Doch das Zurückgeben einer Promise macht den Code nicht asynchron. Alles vor dem ersten await wird zum Zeitpunkt der Aufrufung synchron ausgeführt. Wenn Sie eine aufwändige Berechnung in eine asynchrone Funktion legen und erwarten, dass die Benutzeroberfläche weiterhin reaktiv bleibt, werden Sie enttäuscht sein: Die Funktion gibt zwar letztendlich eine Promise zurück, doch die aufwändige Arbeit beansprucht weiterhin den Thread zuerst. Für wirklich rechenintensive Aufgaben stehen im Browser Web Workers, in Node.js worker_threads oder die Aufteilung der Arbeit in Blöcke zur Verfügung.
Kurz gesagt ist async/await eine Methode zum Schreiben von Promise-Ketten, die von oben nach unten gelesen werden. Es plant Fortsetzungen des Ausführungsflusses an – es lässt niemals blockierenden Code gleichzeitig laufen.
Wo Sie await platzieren können, um unabhängige Aufgaben zu seriellisieren
Zurück zum Dashboard-Lader:
async function loadDashboard(userId) {
const user = await fetchUser(userId);
const projects = await fetchProjects(userId);
const notifications = await fetchNotifications(userId);
return {
user,
projects,
notifications,
};
}
Man geht davon aus, dass fetchProjects() den Parameter user nicht benötigt und fetchNotifications() weder das vorherige Ergebnis noch user. Dennoch führt die Funktion drei unabhängige Anfragen nacheinander aus, da der zweite Aufruf erst erfolgt, wenn die erste Promise erfüllt ist, und die dritte auf die zweite wartet. Die Gesamtverzögerung entspricht der Summe aller drei:
fetch user
████████
fetch projects
███████████
fetch notifications
███████
Diese Lösung wird in der Regel als „Verwendung von Promise.all(), um die Operationen parallel auszuführen“ beschrieben. Eine präzisere Beschreibung lautet, dass alle unabhängigen Operationen bereits vor dem Warten auf ein Ergebnis gestartet werden müssen. Der Aufruf der drei Funktionen erstellt zunächst drei laufende Promises, und erst danach wartet der Code auf das kombinierte Ergebnis:
async function loadDashboard(userId) {
const userPromise = fetchUser(userId);
const projectsPromise = fetchProjects(userId);
const notificationsPromise =
fetchNotifications(userId);
const [user, projects, notifications] =
await Promise.all([
userPromise,
projectsPromise,
notificationsPromise,
]);
return {
user,
projects,
notifications,
};
}
Die Anfragen überschneiden sich nun, und die Gesamtverzögerung entspricht in etwa der des langsamsten Vorgangs:
fetch user
████████
fetch projects
███████████
fetch notifications
███████
all required results available
Promise.all() nimmt eine Sammlung von Promises entgegen und gibt ein Promise zurück, das erfüllt wird, sobald alle Eingaben erfüllt sind. Der Ergebnisarray behält die Reihenfolge der Eingaben bei, unabhängig davon, welche Operation zuerst abgeschlossen wird, sodass die Dekonstruktion in user, projects und notifications weiterhin korrekt bleibt.
Bemerken Sie, dass das, was sequenziellen von parallelen Code unterschied, niemals das Vorhandensein von await war. Es waren die Abhängigkeiten. Wenn ein Schritt tatsächlich die Ausgabe eines anderen benötigt, ist das sequenzielle Warten die richtige Wahl:
const user = await fetchUser(userId);
const permissions = await fetchPermissions(user.role);
Falls eine solche Abhängigkeit nicht existiert, führt das Warten auf jede Operation vor dem Start der nächsten zu Verzögerungen, ohne die Korrektheit zu verbessern. Die sinnvolle Frage bei der Code-Review lautet daher nicht „Sollte man hier Promise.all() verwenden?“, sondern „Welche Operationen hängen von früheren Ergebnissen ab und welche könnten bereits ausgeführt werden?“
Promise.all() koordiniert die Ergebnisse; es storniert keine Arbeiten
Nachdem viele Entwickler Promise.all() entdeckt haben, bilden sie sich eine neue Vorstellung davon, was bei Fehlern geschieht. Nehmen wir diese Version:
const [user, projects, notifications] =
await Promise.all([
fetchUser(userId),
fetchProjects(userId),
fetchNotifications(userId),
]);
Falls fetchProjects() frühzeitig abgelehnt wird, wird die kombinierte Promise umgehend mit diesem Grund abgelehnt, anstatt auf den Rest zu warten. Es ist verlockend zu glauben, dass die anderen beiden Anfragen nun gestoppt werden. Das ist jedoch nicht der Fall. Die Ablehnung der aggregierten Promise storniert nichts, was bereits gestartet wurde – ein Punkt, den MDN ausdrücklich erwähnt. Die anderen Operationen laufen weiter, und ihre Ergebnisse werden einfach ignoriert.
Bei Lesevorgängen führt dies meist nur zu Ressourcenverschwendung. Bei Schreibvorgängen kann es ernsthafte Folgen haben:
await Promise.all([
updateProfile(),
writeAuditLog(),
sendWebhook(),
]);
Falls writeAuditLog() abgelehnt wird, könnte updateProfile() bereits gespeichert worden sein und sendWebhook() könnte bereits unterwegs sein. Die Erfassung der Ablehnung rückgängigt weder das eine noch das andere.
Das ist ein Problem des Systemdesigns, kein Syntaxproblem. Wenn diese Schritte entweder gemeinsam erfolgreich oder gescheitert sein müssen, benötigen Sie einen echten Atomitätsmechanismus: eine Datenbanktransaktion für Änderungen in einer Datenbank oder, bei entfernten Systemen, eine Kombination aus kompensierenden Aktionen, idempotenten Operationen sowie persistierten Workflow-Zuständen. Promise.all() verspricht, Ihnen mitzuteilen, wann eine Gruppe von Promises abgeschlossen ist. Es verspricht jedoch weder einen Rollback noch die Stornierung noch, dass die Nebeneffekte als eine Einheit ablaufen. Diese Garantien müssen von anderer Stelle kommen.
Ein verwandtes Werkzeug ist Promise.allSettled(), das auf jeden Eingang wartet und jedes Ergebnis einzeln meldet. Es ist nützlich, wenn Sie genau wissen müssen, welche Schritte erfolgreich waren, bietet aber ebenfalls keinen Rollback.
try/catch erfasst nur die Ablehnungen, die durch es fließen
Ein Grund dafür, dass async/await an Popularität gewann, ist, dass Promise-Fehler mit dem vertrauten try/catch-Konstrukt behandelt werden können:
async function loadUser(userId) {
try {
const user = await fetchUser(userId);
return user;
} catch (error) {
console.error("Failed to load user", error);
throw error;
}
}
Wenn die wartende Promise abgelehnt wird, wirft die await-Expression den Grund für die Ablehnung innerhalb der Funktion aus, und der zugehörige catch-Block erhält ihn genauso wie bei einer synchronen Ausnahme.
Das bedeutet jedoch nicht, dass try jede asynchrone Operation überwacht, die innerhalb seiner Klammern gestartet wird. Betrachten wir beispielsweise die Erstellung eines Kontos:
async function createAccount(input) {
try {
await saveUser(input);
sendWelcomeEmail(input.email);
return { success: true };
} catch (error) {
console.error(error);
return { success: false };
}
}
saveUser() wird abgewartet, wodurch ein Fehler in den catch-Block gelangt. sendWelcomeEmail() hingegen wird nicht abgewartet. Falls es eine Promise zurückgibt, die später abgelehnt wird, tritt diese Ablehnung niemals in diesen Ablauf ein, da nichts auf diese Promise wartet oder sie zurückgibt. createAccount() könnte bereits zu dem Zeitpunkt, an dem die E-Mail versagt, { success: true } zurückgegeben haben, wodurch der Fehler separat auftaucht – in der Regel als unaufgearbeitete Ablehnung. In aktuellen Versionen von Node.js beenden unaufgearbeitete Ablehnungen standardmäßig den Prozess, sodass es sich hierbei nicht um ein rein kosmetisches Problem handelt.
Das Überspringen von await ist nicht automatisch ein Fehler. Manchmal wird die E-Mail absichtlich außerhalb des Anfragenpfads belassen. In einer Produktionsumgebung wird jedoch eine solche getrennte Verarbeitung in der Regel in einen dauerhaften Warteschlange gespeichert, anstatt als unaufgepasste Promise gestartet zu werden. Der eigentliche Fehler besteht darin, anzunehmen, dass der try-Block aufgrund seiner visuellen Position eine Fehlerabschirmung um zukünftige asynchrone Aufgaben schafft.
Dieselbe Falle tritt am Aufrufort auf. Der Aufrufer scheint geschützt zu sein, doch der Codeausschnitt ist reiner JavaScript ohne await:
try {
loadDashboard(userId);
} catch (error) {
console.error("Dashboard failed");
}
loadDashboard() gibt sofort eine Promise zurück. Jeder asynchrone Fehler lehnt diese Promise später ab, nachdem der try-Block bereits beendet ist, sodass der catch-Block niemals ausgeführt wird. Der Aufrufer muss an der Promise-Kette teilnehmen:
try {
await loadDashboard(userId);
} catch (error) {
console.error("Dashboard failed");
}
Alternativ kann man mit .catch() einen Ablehnungshandler hinzufügen. Die zugrundeliegende Logik ist einfach, sobald man Asynmfunktionen nicht mehr als gewöhnliche Funktionen betrachtet, die zufällig await enthalten – ihre Aufrufer erhalten Promises, und Fehler werden über diesen Promise-Vertrag weitergeleitet.
Absage ist ein separates Protokoll
Stellen Sie sich ein Suchfeld vor, in das der Benutzer Zeichen für Zeichen eintippt:
r
re
rea
reac
react
Eine einfache Implementierung sendet bei jedem Tastendruck eine Anfrage, selbst dann noch, wenn frühere Anfragen noch ausstehen:
async function search(query) {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}`
);
return response.json();
}
In await steht nichts, das besagt, dass die Anfrage für „r“ gestoppt werden sollte, weil jetzt die Anfrage für „react“ wichtig ist. Die frühere Anfrage wird bis zum Abschluss ausgeführt, obwohl die Schnittstelle sie nicht mehr benötigt.
Für fetch wird die Abbruchfunktion mit AbortController und dessen AbortSignal realisiert. Diese Version bricht die vorherige Anfrage ab, bevor eine neue gestartet wird:
let controller;
async function search(query) {
controller?.abort();
controller = new AbortController();
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}`,
{
signal: controller.signal,
}
);
return response.json();
}
Der Controller stellt ein Signal bereit, auf das kompatible APIs lauschen. Durch das Abbrechen dieses Signals wird fetch angewiesen, aufzuhören – sowohl die Anfrage selbst als auch das Lesen des Antwortkörpers werden dabei gestoppt. Ein Punkt, den man berücksichtigen muss: Der abgebrochene Aufruf wirft einen AbortError aus, weshalb jeder, der search() aufruft, diesen Fehler erkennen und ignorieren sollte, anstatt ihn dem Benutzer anzuzeigen.
Der Begriff „kompatible APIs“ ist wichtig. Promises verfügen über keine universelle Abbruchfunktion, und await hat keinen Weg, das zu stoppen, worauf es wartet. Ein Abbruch ist nur möglich, wenn die aufgerufene Operation dies unterstützt und das Signal an den Teil weiterleitet, der die eigentliche Arbeit ausführt.
Ihre eigenen Funktionen können denselben Vertrag übernehmen, indem sie ein Signal entgegennehmen und es zwischen den Schritten prüfen:
async function processFile(file, { signal }) {
for (const chunk of file.chunks) {
signal.throwIfAborted();
await processChunk(chunk);
}
}
signal.throwIfAborted() wirft den Abbruchgrund aus, wenn ein Abbruch angefordert wurde, sodass die Schleife vor der Verarbeitung des nächsten Datensatzes stoppt. Der Abbruch ist nun ein expliziter Bestandteil der Funktionsinterface anstelle davon, dass die Aufrufer hoffen, await würde dies für sie erledigen. Für eine feinere Steuerung können Sie das Signal auch an processChunk() weitergeben, damit ein langer Datensatz bereits nach einer Weile gestoppt werden kann.
Timers folgen derselben Logik. Mit Promise.race() kann man eine Operation gegen einen Timer antreten lassen, wodurch der Aufrufer aufhören kann zu warten – doch die zugrunde liegende Operation setzt sich fort, es sei denn, ihr wird ebenfalls der Stop-Befehl erteilt. Das Beenden der Überwachung und das Beenden der Arbeit sind zwei verschiedene Dinge.
Lokale Sortierung ist keine globale Sortierung
Die sequentielle Verwendung von await garantiert zwar die Reihenfolge innerhalb einer Funktion:
await saveOrder(order);
await sendConfirmation(order);
sendConfirmation() wird erst aufgerufen, nachdem saveOrder() abgeschlossen ist. Diese lokale Garantie sagt jedoch wenig über den Rest des Systems aus. Stellen Sie sich zwei Anfragen vor, die dasselbe Profil aktualisieren. Eine sendet:
await updateProfile({
name: "Umar",
});
Eine paar Millisekunden später sendet eine andere:
await updateProfile({
name: "Umar Dev",
});
Jeder Aufrufer wartet korrekt auf seine eigene Aktualisierung. Nichts davon bestimmt, welche Schreiboperation zuletzt die Datenbank erreicht, wie Wiederholungsversuche auf beiden Seiten ablaufen, ob die beiden Anfragen von verschiedenen Servern verarbeitet werden oder ob veraltete Daten neuere Daten überschreiben.
Die Browser-Version des Problems ist das klassische Suchrennen: Anfrage A beginnt vor Anfrage B, beendet sich aber danach, und der Code, der die zurückgegebenen Ergebnisse anzeigt, zeigt veraltete Ergebnisse auf dem Bildschirm:
const results = await search(query);
render(results);
Kein zusätzliches await kann das beheben. Die Anwendung benötigt eine Regel bezüglich Relevanz oder Reihenfolge: Ältere Suchanfragen müssen storniert werden, Anfragen mit einer Version gekennzeichnet und veraltete Antworten entfernt, Identifikatoren vor der Darstellung verglichen oder Versionskontrollen dort eingeführt, wo die Daten gespeichert sind. Zu beachten ist, dass await den Ablauf innerhalb einer Funktion steuert; es schafft keine globale Reihenfolge für unabhängige asynchrone Operationen.
Wiederholte Versuche zeigen, was await niemals verspricht
Bei Wiederholten Versuchen wird ein unvollständiges mentales Modell teuer. Beginnen wir mit einem Zahlungsauftrag:
async function submitPayment(payment) {
const response = await fetch("/api/payments", {
method: "POST",
body: JSON.stringify(payment),
});
return response.json();
}
Nehmen wir an, die Anfrage läuft ab und ein Entwickler fügt einen naiven Wiederholungsversuch hinzu:
try {
return await submitPayment(payment);
} catch {
return await submitPayment(payment);
}
Dies setzt voraus, dass der fehlgeschlagene Versuch keine Auswirkungen hatte. Ein Timeout bedeutet lediglich, dass der Client nicht rechtzeitig eine Antwort erhalten hat. Der Server könnte bereits die Karte belastet und die Antwort verloren haben, oder die Verbindung könnte abgebrochen sein, nachdem der Nebeneffekt bereits umgesetzt wurde. Blindes Wiederholen kann dazu führen, dass der Kunde zweimal belastet wird.
await kann nicht angeben, ob ein erneuter Versuch sicher ist. Eine Promise gibt an, ob dieser Versuch eine Erfüllung oder eine Ablehnung erbracht hat, die vom Aufrufer beobachtet werden kann. Sie gibt jedoch nicht an, ob das entfernte System vor diesem Ergebnis irreversible Aktionen ausgeführt hat.
Für Operationen mit Nebeneffekten muss die Sicherheit bei erneuten Versuchen bereits in der Operation selbst berücksichtigt werden, in der Regel durch Idempotenz. Der Client erzeugt einmal pro logischem Zahlungsversuch einen stabilen Schlüssel und sendet diesen bei jedem erneuten Versuch mit:
await fetch("/api/payments", {
method: "POST",
headers: {
"Idempotency-Key": paymentAttemptId,
},
body: JSON.stringify(payment),
});
Der Schlüssel ist nur dann nützlich, wenn der Server ihn berücksichtigt: Er muss den Schlüssel zusammen mit dem Ergebnis speichern und, wenn derselbe Schlüssel erneut eingereicht wird, das ursprüngliche Ergebnis zurückgeben anstatt erneut Gebühren zu erheben. Der Schlüssel muss außerdem bei Wiederholungsversuchen derselben Anfrage unverändert bleiben; die Erstellung eines neuen Schlüssels pro Anfrage vereitelt den Zweck. Die Implementierung unterscheidet sich zwischen den Systemen, doch das Prinzip bleibt gleich: Die Semantik der Wiederholungsversuche gehört zur Operation sowie zu den Systemen, die ihre Nebeneffekte ausführen, und nicht zu async/await. Die Serverseitige Handhabung wird in Understanding Idempotency Keys in Node.js POST Endpoints erläutert.
Daraus ergibt sich eine wertvolle Gewohnheit bei der Erstellung von JavaScript-Code. Immer wenn ein angefordertes Aufruf fehlschlägt, sollten zwei Aussagen getrennt bleiben: „Ich habe keinen Erfolg erhalten“ und „die Operation hat definitiv nicht stattgefunden“. Es handelt sich dabei um keine identischen Behauptungen.
Ein kleineres, präziseres mentales Modell
Um gut mit async/await umgehen zu können, müssen Sie sich die ECMAScript-Spezifikation nicht auswendig merken. Es genügt ein einfaches Konzept: Eine asynchrone Funktion gibt eine Promise zurück; ihr Körper läuft normal weiter, bis er auf ein await stößt; das await pausiert die Funktion, bis der erwartete Wert verfügbar ist, und plant anschließend die Fortsetzung.
Alles Weitere ist eine separate Frage mit eigener Antwort:
- Mehrere Operationen: Wann beginnt jede einzelne und hängt sie von einer anderen ab? Das zeigt an, ob die Abfolge beabsichtigt oder zufällig ist.
try/catch das schützt, wofür man es hält.await-Aufrufe es nicht können?Kernpunkte
async/await ist absichtlich einfach gehalten. Es macht den asynchronen Ablaufverlauf lesbar, was äußerst wertvoll ist, doch genau diese Lesbarkeit lässt den Code synchroner erscheinen, als das zugrunde liegende System tatsächlich ist. Wenn Sie auf ein await stoßen, sollten Sie es nicht als „Das Programm wartet hier“ interpretieren. Lesen Sie es vielmehr als Aufforderung: Diese Funktion wird pausiert, bis ein Wert eingehend ist – daher müssen Sie prüfen, welche Operationen bereits laufen, was in der Zwischenzeit ausgeführt werden könnte und welche Garantien Ihr eigenes Design bieten muss. Diese Frage entspricht dem tatsächlichen Funktionsverhalten der Syntax und führt Sie direkt zu den Entwurfsentscheidungen, die Produktionsfehler verhindern.
Weitere Literatur
- Async/Await vs 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 trotzdem auf die reinen Promise-APIs zurückgreifen sollte.
- Häufige Missverständnisse bezüglich async/await, die Produktionsfehler verursachen — Erklärt neun subtile Missverständnisse bezüglich async/await – von Rennbedingungen bis hin zu unverarbeiteten Ablehnungen –, die JavaScript-Anwendungen in der Praxis heimlich zerstören.