Await sieht nur ein Versprechen: Die Entwicklung von Async/Await-APIs, die ihre Aufgaben zuverlässig erledigen
Was JavaScript async/await tatsächlich überwacht, warum abgetrennte Aufgaben, Callbacks, Streams und Promise.all daran vorbeigehen, und wie man Promises entwerfen kann, die anzeigen, dass eine Aufgabe abgeschlossen ist.
Quellen: 90% of Developers Don’t Understand What JavaScript `await` Actually Waits For
Es ist einfach, await als „warten, bis diese Funktion abgeschlossen ist“ zu verstehen, doch die Laufzeit bietet eine weitaus engere Garantie: Sie bewertet eine Ausdruck und behandelt das Ergebnis als einen versprechensähnlichen Wert, wodurch die aktuelle async-Funktion bis zur Festlegung dieses einzigen Wertes pausiert wird. Alles Weitere hängt davon ab, wie dieser Wert erzeugt wurde. Diese Lücke erklärt, warum Code mit sorgfältig platzierten await-Schlüsselwörtern dennoch zu früh zurückgeben, langsamer laufen als nötig, Fehler verschlucken oder auf veraltete Daten reagieren kann. Nachdem Sie sich mit den untenstehenden Fällen auseinandergesetzt haben, werden Sie in der Lage sein, jeden await anzusehen und sich die einzige wichtige Frage zu stellen: Stellt dieses Promise tatsächlich die Arbeit dar, die mich interessiert?
Das Problem: Code, der auf await wartet und scheinbar abgeschlossen ist, es aber nicht ist
Die Fehler in diesem Bereich weisen ein Muster auf. Ein Entwickler wartet auf eine Funktion und geht davon aus, dass alles, was von dieser Funktion gestartet wurde, bereits abgeschlossen ist. Er wartet auf eine Array-Methode und nimmt an, dass jeder asynchrone Callback ausgeführt wurde. Er wartet auf die Einrichtung eines Timers, eine Event-Abonnement oder einen Stream-Write und glaubt, die zugrunde liegende Operation sei beendet. Der Code erscheint korrekt, weil await direkt dort steht.
Aber await hat keinerlei Kenntnis von „der Aufgabe“, „der Anfrage“ oder „allem, was von dieser Funktion gestartet wurde“. Wenn der Ausdruck auf seiner rechten Seite nicht den gesamten Lebenszyklus der Arbeit abdeckt, kann auch das Schlüsselwort nichts dazu beitragen, diesen abzudecken. Wenn man das im Hinterkopf behält, ändert sich die Art und Weise, wie man APIs, Schleifen, Hintergrundaufgaben, ereignisgesteuerten Code sowie Fehlerbehandlung entwirft.
Der Aufruf erfolgt vor dem Warten
Nehmen wir die gewöhnlichste Zeile in asynchronem JavaScript.
const user = await loadUser();
Der Laufzeitumgebung wird nicht zuerst await angezeigt und anschließend höflich von loadUser gebeten, asynchron ausgeführt zu werden. Sie bewertet loadUser() genauso wie jeden anderen Funktionsaufruf. Dieser Aufruf erzeugt einen Wert, der entweder ein natives Promise, ein benutzerdefiniertes thenables-Objekt, ein einfaches Objekt, eine Zeichenkette, undefined oder etwas anderes sein kann. Erst danach kann await mit dem Ergebnis arbeiten.
Die Folge ist, dass die eigentliche Operation in der Regel bereits während des Aufrufs beginnt, noch bevor mit dem Warten begonnen wird. Wenn Sie zwei asynchrone Funktionen aufrufen und ihre Promises aufbewahren, bevor Sie auf eines von ihnen warten, beginnen beide Operationen fast gleichzeitig:
const userPromise = loadUser();
const settingsPromise = loadSettings();
const user = await userPromise;
const settings = await settingsPromise;
Hier beginnt loadUser() mit seiner Arbeit und gibt ein Promise zurück, anschließend startet loadSettings() seine eigene Arbeit. Die beiden await-Zeilen sammeln lediglich die Ergebnisse ein, sobald sie verfügbar sind.
Vergleichen Sie das mit der Version, die die meisten Menschen standardmäßig schreiben:
const user = await loadUser();
const settings = await loadSettings();
Nun wird loadSettings() erst dann aufgerufen, wenn die erste Promise abgeschlossen ist und die Funktion wieder fortgesetzt wird. Die beiden Anfragen werden nacheinander ausgeführt.
Beachten Sie, dass await selbst keine Entscheidung bezüglich des Parallelismus trifft. Entscheidend ist vielmehr die Reihenfolge der Funktionsaufrufe. Eine kurze Erinnerung: Der Aufruf einer asynchronen Funktion startet die Operation; await bestimmt, wann die aktuelle Funktion fortgesetzt wird. Eine Vorsichtsmaßnahme beim ersten Muster: Wenn settingsPromise abgelehnt wird, während Sie noch auf userPromise warten, bleibt diese Ablehnung vorübergehend unverarbeitet, was einige Laufzeiten als Fehler melden. Promise.all vermeidet dieses Problem, indem es beide Promises gleichzeitig überwacht. Für einen detaillierteren Vergleich dieser Ansätze siehe choosing between Promise.all, Promise.race and sequential awaits.
Ausführung eines Promises, nicht aller Nebeneffekte
Betrachten Sie eine Kontenerrichtungsfunktion, die ein Datensatz speichert, eine Willkommens-E-Mail versendet, ohne auf eine Antwort zu warten oder diese zurückzuerhalten, und anschließend das Konto zurückgibt.
async function createAccount(input) {
const account = await accountRepository.save(input);
sendWelcomeEmail(account.email);
return account;
}
Dann schreibt ein Anrufer den offensichtlichen Satz:
const account = await createAccount(input);
Der Aufrufer setzt seine Ausführung fort, sobald die von createAccount zurückgegebene Promise erfüllt wird – das geschieht nach Abschluss der Speicherung und dem Rückgabewert der Funktion. Dabei wird nichts über die E-Mail mitgeteilt. sendWelcomeEmail hat eine separate asynchrone Operation erstellt, deren Promise niemals mit dem von createAccount zurückgegebenen Promise verknüpft wurde; aus Sicht des Aufrufers existiert diese Arbeit somit nicht.
Das ist der Kern des Missverständnisses. await verfolgt lediglich die Kette der Promises, die vom übergebenen Wert aus erreichbar sind. Es durchsucht nicht nach allen von der Funktion gestarteten Aktionen und wartet sicherlich nicht darauf, dass der Prozess inaktiv wird.
Die praktischen Folgen sind dreifach:
- Falls die E-Mail-Promise abgelehnt wird, wird dies vom
try/catch-Block des Aufrufers niemals erkannt. - Falls der Prozess beendet wird, könnte die E-Mail einfach nie versandt werden.
Das bedeutet keineswegs, dass alle Nebeneffekte innerhalb der Anfrage abgeschlossen werden müssen. Viele Aufgaben gehören zu Recht in den Hintergrund. Das eigentliche Problem ist die unklare Zuständigkeit. Wichtige Aufgaben sollten zurückgegeben und abgewartet werden, explizit markiert und als optional behandelt werden oder an ein dauerhaftes Warteschlangen- oder Job-System übergeben werden. Das Erstellen einer Promise und das Ignorieren dessen Ergebnisses ist keine Hintergrundverarbeitung – es ist lediglich Hoffen.
Eine asynchrone Funktion kann abgeschlossen werden, während ihre Unterfunktionen noch laufen
Eine damit verbundene Annahme ist, dass das Schlüsselwort async dazu führt, dass JavaScript jede asynchrone Operation verfolgt, die innerhalb der Funktion gestartet wird. Das ist jedoch nicht der Fall. Eine async-Funktion erzeugt genau einen Promise, und dieser Promise ist nur mit dem verbunden, was die Funktion explizit zurückgibt oder auf ihren eigenen Kontrollfluss wartet. Alles andere kann weiterlaufen, nachdem dieser Promise abgeschlossen wurde.
Die klassische Veranschaulichung verwendet forEach:
async function processUsers(users) {
users.forEach(async (user) => {
await updateUser(user);
});
}
Jede Aufrufung des asynchronen Callbacks gibt ein Promise zurück, doch forEach wirft diese Rückgabewerte weg. Es sammelt sie nicht, wartet auf sie oder gibt sie weiter; es gibt stattdessen undefined zurück. Daher erreicht processUsers seine Schließungsklammer fast sofort, und da eine async-Funktion, die nach ihrem Ende ausgeführt wird, mit undefined abgeschlossen wird, setzt der Aufrufer seine Arbeit schon lange vor Abschluss jeder Aktualisierung fort:
await processUsers(users);
console.log("All users processed");
Die Aussage ist einfach falsch. Der Aufrufer hat darauf gewartet, dass processUsers mit dem Planen der Callbacks fertig wird, nicht mit der Ausführung der Callbacks selbst.
Um das zu beheben, müssen die untergeordneten Promises in die übergeordnete Operation eingebunden werden. Je nach Situation bedeutet das eine for...of-Schleife mit await innerhalb, um eine strenge Abfolge zu gewährleisten, Promise.all(users.map(updateUser)) für vollständige Parallelität oder einen Konkurrenzbeschränker, wenn die Menge groß genug ist, um eine Datenbank oder ein API zu überlasten. Die allgemeine Regel gilt weit über forEach hinaus: Eine übergeordnete Promise wartet nur auf die Arbeit der untergeordneten Promises, die Teil ihrer Kette sind. Werkzeuge wie die Regeln no-floating-promises und no-misused-promises in typescript-eslint erkennen viele dieser Fehler automatisch.
Callback-APIs sind für await unsichtbar
Ein großer Teil des JavaScript-Ökosystems existiert bereits vor Promises und meldet die Fertigstellung über Callbacks. Das führt zu Code wie diesem:
await setTimeout(() => {
console.log("Timer completed");
}, 1000);
Es macht keine Sekunde Pause. setTimeout registriert den Callback und gibt sofort einen Timer-Handle zurück (eine Zahl in Browsern, ein Timeout-Objekt in Node.js). Dieser Handle ist keine Promise, die angibt, wann der Timer auslöst. await erhält einen gewöhnlichen Wert, behandelt ihn als bereits verfügbar und setzt die Funktion beim nächsten Microtask fort – lange bevor der Callback ausgeführt wird.
Dieselbe Falle tritt bei Event-Listenern, älteren Dateisystem-APIs, Datenbanktreibern, Streams sowie selbst entwickelten SDKs auf. Wenn eine Funktion eine Abonnement-Referenz, einen Anfrage-Handle, ein Boolesches Wert oder einen Event-Emitter zurückgibt, macht das Warten auf diesen Rückgabewert den eventualen Callback nicht zur Erfüllung einer Promise.
Um auf auf Callbacks basierendes Verhalten warten zu können, benötigen Sie eine Promise, die genau dann erfüllt oder abgelehnt wird, wenn der Callback das Ergebnis meldet:
function delay(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
await delay(1000);
Nun steht der erwartete Wert tatsächlich für das Ereignis, das Sie interessiert: Die Promise bleibt unerledigt, bis der Timer die Methode resolve aufruft. Moderne Plattformen liefern oft bereits solche Wrapper; Node.js bietet beispielsweise eine auf Promises basierende setTimeout in node:timers/promises sowie util.promisify für Funktionen im Callback-Stil.
Dies bringt die hinter jedem await verborgene Konstruktionsfrage zum Vorschein: Stellt der zurückgegebene Wert die Vollendung dar oder lediglich die Registrierung, den Start oder einen Handle für noch laufende Arbeiten? Falls es sich um eines der Letzteren handelt, wird das Warten darauf nicht auf das von Ihnen erwartete Ergebnis führen.
Thenables, einfache Werte und die Grenze der Microtasks
await verlangt keine echte Promise-Instanz. Wenn der Wert ein Thenable ist, also ein Objekt mit einer aufrufbaren .then-Methode, übernimmt await die Abwicklung dieses Objekts. Dadurch können Promise-kompatible Bibliotheken sowie benutzerdefinierte asynchrone Objekte direkt in async-Funktionen eingebunden werden, und ein Objekt kann asynchron handeln, ohne jemals new Promise verwenden zu müssen.
Für Werte, die keine Thenables sind, fügt await dennoch einen asynchronen Schritt ein:
async function example() {
console.log("before");
await 42;
console.log("after");
}
console.log("start");
example();
console.log("end");
Dies gibt start, before, end aus und erst danach after, obwohl 42 sofort verfügbar war. Die Funktion gibt beim await den Wert ab, der synchronen Code nach dem Aufruf setzt sich fort, und der Rest von example wird später als Microtask ausgeführt.
Daher ist das Warten auf einen einfachen Wert nicht synchron. Es kann die Reihenfolge Ihres Codes in Bezug auf umliegende synchrone Anweisungen sowie andere in der Warteschlange befindliche Microtasks verändern, und in reihenfolgeempfindlichen oder belasteten Abläufen fügt ein unnötiges await eine Scheduling-Grenze hinzu. Die Verzögerung ist in Bezug auf die Leistung in der Regel vernachlässigbar, doch der konzeptionelle Punkt bleibt bestehen: Code nach await setzt sich niemals auf derselben ununterbrochenen Aufrufstapel fort.
Ablehnung ist Teil des Abwickelns
Menschen sagen oft, await warte darauf, dass eine Promise abgeschlossen wird. Genauer ist es, zu sagen, es warte darauf, dass die Promise sich entscheidet – wobei sich entscheiden auch die Ablehnung beinhaltet. Wenn die erwartete Promise abgelehnt wird, wirft await den Grund für die Ablehnung innerhalb der aktuellen async-Funktion aus, wodurch normale try/catch-Strukturen für asynchrone Fehler funktionieren:
try {
const order = await loadOrder(orderId);
return order;
} catch (error) {
logger.error(error);
throw error;
}
Diese Schutzmaßnahme gilt nur dann, wenn die abgelehnte Promise tatsächlich innerhalb des try-Blocks abgewartet wird. Eine erstellte Promise, die unverarbeitet bleibt, entzieht sich ihr vollständig:
try {
loadOrder(orderId);
} catch (error) {
console.error("This does not catch the Promise rejection");
}
Hier war der Aufruf selbst erfolgreich: Er gab eine Promise zurück, ohne einen Fehler auszulösen. Die Ablehnung tritt später über diese Promise ein, nachdem der try-Block bereits längst beendet wurde.
Das ist besonders gefährlich, wenn await absichtlich weggelassen wird, um etwas „nicht blockierend“ zu machen, obwohl der Entwickler weiterhin glaubt, dass die umgebenden try/catch-Blöcke es schützen. Das ist jedoch nicht der Fall. Jede asynchrone Operation benötigt einen klaren Verantwortlichen für ihren Fehlerfall: einen Aufrufer, der darauf wartet oder sie zurückgibt, einen expliziten .catch-Handler oder ein System, das die Verantwortung übernimmt. Ein Promise ohne Verantwortlichen bedeutet keine schnellere Architektur – es handelt sich vielmehr um einen Fehlerweg, der vom Code abgeschnitten ist, der versteht, was schiefgelaufen ist. Der Artikel zu was async/await tatsächlich garantiert untersucht denselben Aspekt der Verantwortlichkeitszuweisung aus der Sicht der Fehlerbehandlung.
Promise.all koordiniert Ergebnisse, nicht Ausgabewerte
Promise.all wird in der Regel als „Auf Warten auf mehrere Promises gleichzeitig“ zusammengefasst. Das ist als erste Annäherung in Ordnung, doch die Details sind wichtig. Es gibt ein neues Promise zurück, das erfüllt wird, sobald alle Eingabewerte erfüllt sind, und abgelehnt wird, sobald ein einziger Eingabewert abgelehnt wird.
const results = await Promise.all(tasks);
Auch hier hat await nur ein Ziel: die zusammengefasste Promise, die von Promise.all erstellt wurde.
Wenn eine der Aufgaben frühzeitig fehlschlägt, versagt auch diese zusammengefasste Promise, und Ihre Funktion folgt dem Fehlerweg. Die verbleibenden Aufgaben werden nicht abgebrochen. Sie laufen weiter und können nachdem der Aufrufer die Batch bereits als fehlgeschlagen markiert hat, immer noch Nebeneffekte erzeugen. Stellen Sie sich eine Batch vor, die mehrere externe Aktualisierungen auslöst: Eine versagt sofort, der Aufrufer meldet „Batch fehlgeschlagen“, während die anderen Aktualisierungen stillschweigend erfolgreich sind und das System nur teilweise verändern.
Promise.all bietet Ihnen die Koordination der Ergebnisse, nicht Transaktionen oder Abbrüche. Wählen Sie das Werkzeug, das den von Ihnen benötigten Semantiken entspricht:
- Falls ein teilweiser Erfolg akzeptabel ist, sammeln Sie jedes Ergebnis (zum Beispiel mit
Promise.allSettled) und überprüfen Sie jedes davon. - Falls Sie ein Voll-oder-Nichts-Verhalten benötigen, erfordern die zugrunde liegenden Operationen einen echten Rollback- oder Kompensationsmechanismus.
- Falls die verbleibende Arbeit nach dem ersten Fehler stoppen soll, muss jede Operation die Stornierung unterstützen, in der Regel über einen gemeinsamen
AbortSignal.
Die kombinierte Promise kann Arbeiten, die bereits begonnen haben, nicht rückgängig machen. Ihre Erfüllung bedeutet nur das, was ihre Konstruktion zulässt.
Streams und langlaufende Ressourcen
Streams sind eine weitere häufige Quelle der Verwirrung. Die write-Methode eines Streams kann ein Boolean zurückgeben, das angibt, ob der interne Puffer weitere Daten aufnehmen kann. Ein Aufruf von .pipe() gibt den Zielstream zurück. Ein ereignisbasiertes Parser kann bereits dann Ergebnisse liefern, sobald seine Zuhörer hinzugefügt wurden. Das Warten auf einen dieser Werte bedeutet nicht, dass man auf das Ende des Streams, das Ausspülen, Schließen oder das Schreiben jedes Bytes wartet.
Die Fertigstellung wird in der Regel durch Ereignisse wie finish, end, close oder error signalisiert, und um sie zuverlässig abzuwarten, müssen diese Ereignisse in eine Promise umgewandelt werden. Viele Bibliotheken sowie Node.js selbst über pipeline in node:stream/promises bieten Hilfsfunktionen, die eine einzige Promise erzeugen, deren Abwicklung den gesamten Pipeline-Prozess einschließlich der darin auftretenden Fehler abdeckt.
Das Prinzip gilt allgemein für jede langfristig verfügbare Ressource:
- Das Starten eines Uploads bedeutet nicht dessen Abschluss.
- Das Öffnen einer Verbindung bedeutet nicht die Beendigung einer Abfrage.
- Das Planen einer Aufgabe bedeutet nicht deren Ausführung.
- Das Einreihen einer Nachricht bedeutet nicht deren Verarbeitung.
Der Wert, den eine API zurückgibt, bestimmt, was await beobachten kann. Bevor man await verwendet, sollte man herausfinden, welche Lebenszyklusgrenze dessen Promise tatsächlich markiert.
Fertigstellung bedeutet keine Relevanz
Stellen Sie sich ein Suchfeld vor, in das ein Benutzer „JavaScript“ eingibt und es anschließend schnell in „TypeScript“ ändert. Zwei Anfragen werden gesendet. Die Anfrage für TypeScript kommt zuerst an und das Bildschirm zeigt die richtigen Ergebnisse; danach erreicht die langsamere Antwort für JavaScript die Seite und überschreibt sie.
Jede Funktion in diesem Ablauf hat await korrekt verwendet. Der Fehler entsteht, weil Warten und Relevanz unterschiedliche Aspekte sind. await setzt seine Ausführung fort, sobald eine Promise abgeschlossen ist; es prüft nicht, ob jemand das Ergebnis noch benötigt, ob eine neuere Anfrage es ersetzt hat oder ob das Komponenten jetzt ein anderes Element anzeigt. Code, der sequenziell erscheint, ordnet nur die Schritte innerhalb einer einzigen Aufrufung, während mehrere Aufrufe derselben Funktion weiterhin frei übereinanderlaufen können.
Um dieses Problem zu lösen, ist ein expliziter Begriff dafür notwendig, welche Operation gerade ausgeführt wird. Sie können veraltete Anfragen abbrechen, jede Antwort mit dem neuesten Anfragen-Identifikator vergleichen, Versionskontrollen verwenden oder sich auf eine Datenerfassungsschicht verlassen, die Antworten nach den Parametern sortiert, die sie erzeugt haben. await kann nicht erkennen, welches Ergebnis zum aktuellen Zeitpunkt gehört. Für eine schrittweise Anleitung mit Fokus auf React siehe Wie man Rennbedingungen behebt – Debouncing hilft nicht.
Dass eine Anfrage abgeschlossen wurde, bedeutet nicht, dass das System konsistent ist
Eine Datenbankschreiboperation kann erfolgreich sein, während die Replikate noch zurückliegen. Die Veröffentlichung einer Nachricht wird in der Regel gelöst, sobald der Broker sie akzeptiert, und nicht, sobald ein Verbraucher sie verarbeitet hat. Das Versenden einer E-Mail wird in der Regel gelöst, wenn der Anbieter die Anfrage akzeptiert, und nicht, wenn jemand sie erhält. Es ist leicht, in await mehr Bestätigung zu sehen, als die API tatsächlich bietet.
await publishEvent(event);
Diese Zeile kann garantieren, dass die Nachricht den Broker erreicht hat. Das bedeutet jedoch nicht unbedingt, dass die darunterliegenden Prognosen, Benachrichtigungen oder Suchindizes bereits aktualisiert wurden.
await sendEmail(message);
Ebenso kann dies nur bedeuten, dass der Anbieter die Nachricht erhalten hat. Es garantiert weder die Zustellung noch, dass der Empfänger sie gelesen hat, noch sogar, dass der Anbieter sie nach der späteren Verarbeitung aufbewahrt hat.
Was „erledigt“ bedeutet, wird durch den Vertrag der API festgelegt, die die Promise erzeugt, und await kann diesen Vertrag nicht stärken. Probleme entstehen, wenn die nächste Zeile davon ausgeht, dass bereits eine umfassendere Zustandsänderung stattgefunden hat – beispielsweise ein Endpunkt, der ein Ereignis veröffentlicht und sofort ein von einem Verbraucher gepflegtes Lesemodell abruft, in der Erwartung, dass die neuen Daten bereits vorhanden sind. Die Promise wurde korrekt erfüllt; die Architektur hinkt einfach noch hinterher. Je nach Produkt könnte ein geeignetes Design einen ausstehenden Status zurückgeben, stattdessen aus dem Schreibmodell lesen, auf eine explizite Bestätigung warten oder in der Benutzeroberfläche eine eventuelle Konsistenz berücksichtigen.
Design-Promises, die zu ihrem Namen passen
Die meisten Verwirrungen bezüglich await verschwinden, sobald man sich eine Frage zur Gewohnheit macht: Stellt der Wert, auf den ich warte, das Abschluss des gesamten Vorgangs dar, der mich interessiert? Wenn man sich die früheren Fälle ansieht, ist die Antwort eindeutig nein in den folgenden Situationen:
- eine Funktion startet Unter-Promises, ohne sie zurückzugeben oder abzuwarten
- eine Array-Methode verwirft die Promises, die ihre asynchronen Callbacks zurückgeben
- eine API gibt einen Handle zurück und meldet den Abschluss über Ereignisse
- ein Sender wird vor den Verbrauchern abgeschlossen
Das Vorhandensein von await beweist lediglich, dass die aktuelle Funktion pausiert hat, bis ein promise-ähnlicher Wert festgelegt wurde. Es beweist nicht, dass die damit verbundenen Arbeiten abgeschlossen sind, dass Nebeneffekte genau einmal aufgetreten sind, dass überlagerte Arbeiten storniert wurden, dass Fehler weiterhin bestehen, oder dass das Ergebnis noch aktuell ist.
Deshalb ist das richtige Gestalten von Asynchroncode größtenteils ein Problem des API-Designs. Die Promise, die eine Funktion zurückgibt, sollte zum Zeitpunkt gelöst werden, der der Verantwortung entspricht, die ihr Name verspricht. Eine Funktion mit dem Namen saveDocument sollte klar machen, ob ihre Promise die lokale Speicherung, die entfernte Synchronisierung oder beides abdeckt. Eine Funktion mit dem Namen scheduleReport kann zu Recht bei der Annahme der Aufgabe gelöst werden anstelle bei der Fertigstellung des Berichts, doch ihr Name und ihr Rückgabetyp sollten das klar machen – beispielsweise durch eine Lösung mit einem Aufgabenerkennungswert, den der Aufrufer abfragen oder verfolgen kann.
Was await versprechen kann und was nicht
awaitbewertet eine Ausdrucksformel und wartet darauf, dass dieser einzelne Wert gelöst wird; es weiß nichts über die „Aufgabe“ als Ganzes.- Der Ort, an dem Sie asynchrone Funktionen aufrufen, bestimmt, ob die Arbeit parallel ausgeführt wird;
awaitbestimmt lediglich, wann Sie fortfahren. - Arbeit, die weder zurückgegeben noch abgewartet wird, wird getrennt: Ihre Fehler, Zeiteinstellungen und Fertigstellung sind für den Aufrufer unsichtbar.
- Callback-, Event- und Stream-APIs müssen in Promises eingehüllt werden, die erst bei tatsächlicher Fertigstellung eine Bedeutung für
awaithaben. Promise.allstorniert weder etwas noch führt es zurück; wählen Sie je nach benötigter SemantikallSettled, Kompensation oderAbortSignal.- Fertigstellung, Relevanz und Konsistenz sind drei verschiedene Garantien, wobei nur die erste von
awaitgewährleistet wird. - Betrachten Sie jeden von Ihnen zurückgegebenen Promise als Vertrag und stellen Sie sicher, dass seine Abwicklung genau das bedeutet, was der Funktionsname andeutet.
Weitere Literatur
- Refactoring von Promise-Ketten zu Async/Await ohne Verhaltensänderung: Wie man .then()-Ketten in lesbaren async/await-Code umwandelt, was await tatsächlich pausiert, wann Schritte parallel ausgeführt werden sollten und wie try/catch .catch() ersetzt.
- 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.