JavaScript Promises
JavaScript Promises erklärt: pending, fulfilled und rejected, die Executor-Funktion mit resolve und reject, then/catch/finally und Microtask-Timing, mit ausführbaren Beispielen.
Promises in JavaScript sind ein leistungsstarkes Werkzeug zur Verwaltung asynchroner Operationen und ermöglichen es Entwicklern, saubereren und robusteren Code zu schreiben. Dieses Kapitel erklärt, was ein Promise ist, welche drei Zustände es annehmen kann, wie die Executor-Funktion mit resolve und reject funktioniert, wie man Ergebnisse mit .then, .catch und .finally verarbeitet und wann Promise-Callbacks tatsächlich ausgeführt werden (die Microtask-Warteschlange). Das Verständnis dieser Grundlagen ist unverzichtbar, bevor man zu Chaining, der Promise API und async/await übergeht.
Einführung in JavaScript Promises
Ein Promise ist ein object, das einen Wert repräsentiert, der möglicherweise noch nicht verfügbar ist, aber es zu einem späteren Zeitpunkt sein wird. Anstatt einen Callback in eine asynchrone Funktion zu übergeben und zu hoffen, dass er aufgerufen wird, erhält man sofort ein Promise-object und hängt Callbacks daran. Das vermeidet tief verschachtelte Callbacks, oft als „Callback Hell" bezeichnet, und bietet eine einheitliche, konsistente Methode zur Behandlung von Erfolg und Fehlschlag.
Eine typische asynchrone Aufgabe — eine Netzwerkanfrage, ein Timer, das Lesen einer Datei — hat ihr Ergebnis nicht sofort bereit. Das Promise ist ein Platzhalter für dieses Ergebnis.
Die drei Zustände eines Promise
Ein Promise befindet sich immer in genau einem von drei Zuständen:
- pending — der Anfangszustand; die Operation wurde noch nicht abgeschlossen.
- fulfilled — die Operation wurde erfolgreich abgeschlossen und das Promise hat einen Wert.
- rejected — die Operation ist fehlgeschlagen und das Promise hat einen Grund (normalerweise ein
Error).
Ein pending Promise kann in den Zustand fulfilled oder rejected übergehen. Sobald dies geschehen ist, ist es settled und kann seinen Zustand nicht mehr ändern. Dieser einseitige, einmalige Übergang macht Promises vorhersehbar: Ein .then-Callback, der an ein bereits erfülltes Promise angehängt wird, wird trotzdem ausgeführt, und ein Promise kann nie von fulfilled zurück zu rejected wechseln.
┌─────────────┐ resolve(value) ┌─────────────┐
│ pending │ ────────────────▶ │ fulfilled │
new Promise ─▶ │ │ └─────────────┘
│ │ reject(reason) ┌─────────────┐
└─────────────┘ ────────────────▶ │ rejected │
└─────────────┘Ein Promise erstellen
Um ein Promise zu erstellen, ruft man den Promise-Konstruktor auf und übergibt ihm eine Funktion, die als Executor bezeichnet wird. Der Executor läuft sofort und synchron, sobald das Promise erstellt wird. Er erhält zwei Funktionen als Argumente, die üblicherweise resolve und reject genannt werden:
- Rufe
resolve(value)auf, um das Promise mitvaluezu erfüllen. - Rufe
reject(reason)auf, um das Promise mitreasonabzulehnen.
Solange keiner von beiden aufgerufen wird, bleibt das Promise pending.
Der Executor erhält resolve und reject, damit er das Promise abschließen kann, sobald die asynchrone Arbeit beendet ist. Hier wird das Promise nach einem einsekundigen Timer erfüllt:
Hinweis: Nur der erste Aufruf von
resolveoderrejectist relevant. Sobald ein Promise settled ist, werden alle weiteren Aufrufe vonresolveoderrejectignoriert. Wenn der Executor synchron einen Fehler wirft, wird das Promise automatisch mit diesem Fehler abgelehnt.
Ergebnisse verarbeiten mit .then, .catch und .finally
Sobald ein Promise erstellt wurde, verarbeitet man sein Ergebnis mit den Methoden .then, .catch und .finally. So reagiert der Rest des Codes auf ein settled Promise.
Die Methode then
Die Methode .then wird verwendet, um einen Callback zu planen, der ausgeführt wird, wenn das Promise erfüllt wird. Damit ein Promise erfüllt wird, muss die resolve-Methode aufgerufen werden. Das Argument, das man an resolve übergibt, wird der endgültige Wert des Promise.
In diesem Code wird das Promise erst nach dem 1000-ms-Timeout erfüllt, wenn die resolve-Methode mit „Done!" aufgerufen wird.
Die Funktion im then-Teil wird nur ausgeführt, nachdem die resolve-Methode aufgerufen wurde.
.then kann auch ein zweites Argument annehmen — einen Ablehnungs-Handler — aber die Verwendung eines separaten .catch (weiter unten) ist übersichtlicher und fängt auch Fehler aus früheren Handlern ab.
Die Methode .catch
Die Methode .catch wird verwendet, um das Promise zu behandeln, falls es abgelehnt wird. Das bedeutet, dass entweder ein Fehler im Promise-Funktionsblock geworfen wird oder die reject-Methode aufgerufen wird.
Für umfangreichere Fehlerbehandlungsmuster — Ablehnung gegenüber geworfenen Fehlern, erneutes Werfen und Wiederherstellen innerhalb einer Kette — siehe Fehlerbehandlung mit Promises.
Die Methode .finally
Die Methode .finally ermöglicht es, Code auszuführen, nachdem das Promise settled ist, unabhängig von seinem Ergebnis. Sie erhält keine Argumente (sie weiß nicht, ob das Promise erfüllt oder abgelehnt wurde) und gibt das Ergebnis oder den Fehler unverändert weiter — sie ist daher ideal für Aufräumarbeiten wie das Ausblenden eines Lade-Spinners.
Wann werden Promise-Callbacks ausgeführt? (Microtasks)
Eine häufige Überraschung ist, dass .then-, .catch- und .finally-Callbacks niemals synchron ausgeführt werden, selbst wenn das Promise bereits settled ist. Sie werden in die Microtask-Warteschlange eingereiht, die die Engine erst verarbeitet, nachdem der aktuelle synchrone Code abgeschlossen ist.
Das bedeutet, dass synchroner Code immer zuerst ausgeführt wird und Promise-Callbacks vor Timern (setTimeout) laufen, die sich in der separaten Macrotask-Warteschlange befinden.
Obwohl das Promise sofort aufgelöst wird und das Timeout 0 beträgt, wird der Promise-Callback (3) vor dem Timeout-Callback (4) ausgeführt, weil die Microtask-Warteschlange vollständig geleert wird, bevor die nächste Macrotask ausgeführt wird.
Daten von einer API mit Promises abrufen
Dieses Beispiel zeigt, wie man mit Promises Daten von einer externen API abruft.
Promises verketten
Promise-Chaining ist eine leistungsstarke Funktion, mit der man mehrere asynchrone Operationen miteinander verknüpfen kann. Jedes .then gibt ein neues Promise zurück, und was auch immer man aus einem Handler returnt, wird zum Erfüllungswert dieses neuen Promise — deshalb trägt das folgende Beispiel einen Wert durch mehrere Schritte. Weitere Informationen finden sich unter JavaScript: Promises und Chaining.
async/await mit JavaScript Promises kombinieren
Die effektive Verwendung von async/await kann die Handhabung asynchroner Operationen vereinfachen und den Code übersichtlicher und leichter verständlich machen, während die volle Leistungsfähigkeit von JavaScript Promises erhalten bleibt. Mehr darüber erfährt man unter JavaScript async/await, aber hier ist ein einfaches Beispiel.
Fazit
Die Beherrschung von JavaScript Promises ist für jeden Entwickler entscheidend, der asynchrone Operationen effizient verwalten möchte. Das grundlegende Modell sollte man im Gedächtnis behalten: Ein Promise ist pending, bis der Executor resolve oder reject aufruft; danach ist es settled für immer; das Ergebnis liest man mit .then/.catch/.finally; und diese Callbacks werden immer als Microtasks ausgeführt, nach dem aktuellen synchronen Code.
Wie geht es weiter?
- JavaScript: Promises und Chaining — asynchrone Schritte sequenzieren und Werte weitergeben.
- Die Promise API — statische Hilfsmethoden wie
Promise.all,Promise.race,Promise.allSettledundPromise.resolve. - Fehlerbehandlung mit Promises — Ablehnungen, geworfene Fehler und Wiederherstellungsmuster.
- JavaScript async/await — Promise-basierten Code schreiben, der wie synchroner Code gelesen werden kann.