Ein Promise ist der Platzhalter für einen zukünftigen Wert
Wenn JavaScript etwas erledigen muss, das Zeit braucht – ein Netzwerk-Request, das Lesen einer Datei oder das Warten auf einen Timer – kann das Ergebnis nicht sofort zurückgegeben werden. Stattdessen bekommst du ein JavaScript Promise: ein Objekt, das für einen Wert steht, der irgendwann vorliegen wird.
Der erste console.log zeigt ein Promise im Zustand pending. Eine halbe Sekunde später wird das Promise erfüllt, und der Callback in .then läuft mit dem Wert. Das Promise an sich ist nur ein Objekt – der Clou ist, dass es weiß, wie es alle Interessenten benachrichtigt, sobald sein Wert vorliegt.
Die drei Zustände eines Promise
Ein Promise befindet sich immer in genau einem von drei Zuständen:
- pending – die Arbeit läuft noch. Es gibt noch keinen Wert.
- fulfilled – die Arbeit war erfolgreich. Ein Wert steht zur Verfügung.
- rejected – die Arbeit ist fehlgeschlagen. Ein Fehler liegt vor.
Ein Promise wechselt genau einmal von pending nach fulfilled oder rejected und bleibt dann für immer in diesem Zustand. Man kann ein Promise weder rückgängig machen noch zweimal auflösen.
Promise.resolve(value) erzeugt ein bereits erfülltes Promise; Promise.reject(error) liefert ein bereits abgelehntes. Praktisch für Tests und immer dann, wenn eine Funktion manchmal sofort ein Ergebnis liefern kann, aber trotzdem ein Promise zurückgeben soll.
Den Wert auslesen: .then und .catch
An den Wert eines Promise kommst du nicht direkt heran – stattdessen übergibst du .then einen Callback, den das Promise aufruft, sobald das Ergebnis bereitsteht:
.catch(fn) greift, sobald das Promise rejected wird. Intern ist das nur eine Kurzform für .then(undefined, fn). Ein .catch() am Ende der Kette fängt Fehler aus jedem vorangehenden Schritt ab — du musst es also nicht hinter jedes .then setzen.
Promise Chaining: Jedes .then gibt ein neues Promise zurück
Genau an dieser Stelle steigen viele aus. .then() führt nicht einfach nur einen Callback aus — es liefert ein neues Promise zurück, das zu dem Wert aufgelöst wird, den der Callback zurückgegeben hat. Dadurch lässt sich überhaupt erst verketten:
Jeder Schritt liefert die Grundlage für den nächsten. Gibt ein .then-Callback selbst ein Promise zurück, wartet die Kette auf dieses Promise, bevor es weitergeht — so lassen sich asynchrone Schritte sauber aneinanderreihen:
Drei asynchrone Schritte hintereinander – ganz ohne Verschachtelung. Wenn du dieselbe Logik mal mit Callbacks schreibst, wird sofort klar, warum sich Promises durchgesetzt haben.
Fehler wandern die Kette entlang
Ein abgelehnter Promise überspringt jedes .then, bis er auf ein .catch trifft. Mehr steckt hinter der Fehlerbehandlung eigentlich nicht:
Wenn du innerhalb eines .then einen Fehler wirfst, wird das Promise, das dieses .then zurückgegeben hat, abgelehnt. Das nächste .then sieht diese Ablehnung und reicht sie weiter, bis sie von .catch aufgefangen wird. In der Regel reicht ein einziges .catch am Ende der Kette völlig aus – und eine Kette ganz ohne .catch erzeugt die berüchtigte Warnung "unhandled promise rejection", die du unbedingt beheben solltest.
Eigene Promises mit new Promise erstellen
Meistens arbeitest du mit Promises, die dir Bibliotheken schon fix und fertig liefern. Hin und wieder musst du aber selbst etwas in ein Promise verpacken – typischerweise, wenn du eine Callback-API im alten Stil in ein Promise umwandeln willst:
Die Funktion, die du an new Promise übergibst, heißt Executor. Sie bekommt zwei Argumente: resolve (damit rufst du bei Erfolg den Wert zurück) und reject (damit meldest du einen Fehler). Du solltest genau eine der beiden Funktionen aufrufen – und zwar nur einmal. Alle weiteren Aufrufe werden ignoriert.
Zwei Angewohnheiten, die dir später viel Ärger ersparen:
- Greif nur dann zu
new Promise, wenn du etwas einpackst, das noch nicht auf Promises basiert. Gibt eine Funktion schon ein Promise zurück, reichst du es einfach weiter. - Ruf
rejectimmer mit einemError-Objekt auf, nie mit einem String. Stacktraces sind Gold wert.
Parallele Ausführung mit Promise.all
.then-Ketten laufen nacheinander ab. Wenn du mehrere unabhängige asynchrone Tasks hast und sie gleichzeitig laufen lassen willst, ist Promise.all das Mittel der Wahl:
Alle drei Timer laufen parallel. Promise.all liefert ein Array mit den Ergebnissen in derselben Reihenfolge wie die Eingabe – sobald jede einzelne Promise erfüllt ist. Die Gesamtzeit liegt damit bei rund 400 ms statt 900 ms.
Der Haken: Promise.all bricht sofort ab, sobald irgendeine der Promises rejected – die übrigen Ergebnisse sind dann verloren. Genau dieses Verhalten willst du, wenn du wirklich alle Teile brauchst (z. B. beim Rendern einer Seite, die auf drei API-Aufrufe angewiesen ist). Wenn nicht, greif zu allSettled.
Wenn einzelne Fehler verkraftbar sind: Promise.allSettled
Promise.allSettled wartet, bis jede Promise durch ist – egal ob fulfilled oder rejected – und liefert dir anschließend einen kompletten Statusbericht:
Jedes Ergebnis ist ein Objekt der Form { status: "fulfilled", value } oder { status: "rejected", reason }. Praktisch ist das immer dann, wenn ein Teilerfolg völlig ausreicht – etwa beim Loggen eines Event-Batches, beim Laden mehrerer Thumbnails oder bei unabhängigen Health-Checks.
Zwei weitere Kombinatoren solltest du kennen:
Promise.race([...])– wird erfüllt, sobald das erste Promise fertig ist, egal ob erfolgreich oder mit Fehler. Ideal für Timeouts.Promise.any([...])– liefert den ersten Erfolg und ignoriert alle Fehlschläge. Schlägt nur dann fehl, wenn wirklich jedes Promise ablehnt.
Promises laufen immer asynchron
Selbst ein bereits aufgelöstes Promise ruft seinen .then-Callback asynchron auf – nie synchron, nie im selben Tick:
Die Ausgabe lautet vorher, nachher, sofort. Der .then-Callback wartet, bis der aktuelle Code durchgelaufen ist, und wird dann über die Microtask-Queue ausgeführt. Genau diese Regel – „ein Promise-Callback läuft nie synchron" – sorgt dafür, dass sich Promises im Zusammenspiel mit synchronem Code vorhersehbar verhalten: Der synchrone Teil ist immer zuerst fertig.
Als Nächstes: async/await
Verkettete .then-Aufrufe funktionieren zwar, aber ab drei oder vier Schritten sieht das Ganze schnell aus wie eine Treppe. async/await ist syntaktischer Zucker auf Basis von Promises und erlaubt es dir, dieselbe Logik zu schreiben, als wäre sie synchron – mit try/catch für die Fehlerbehandlung und normalen Variablen für Zwischenergebnisse. Darum geht es im nächsten Kapitel.
Häufig gestellte Fragen
Was ist ein Promise in JavaScript?
Ein Promise ist ein Objekt, das für einen Wert steht, der noch nicht da ist – typischerweise das Ergebnis einer asynchronen Operation wie einem Netzwerk-Request. Ein Promise befindet sich immer in einem von drei Zuständen: pending, fulfilled oder rejected. An den späteren Wert kommst du über Callbacks, die du mit .then() und .catch() anhängst.
Was ist der Unterschied zwischen then und catch?
.then(onFulfilled) läuft, wenn das Promise erfolgreich aufgelöst wird, und bekommt den aufgelösten Wert übergeben. .catch(onRejected) springt an, sobald das Promise – oder irgendein Promise weiter oben in der Kette – rejected wird, und erhält den Fehler. Ein einziges .catch() am Ende der Kette fängt also Fehler aus allen vorherigen Schritten ab.
Was macht Promise.all?
Promise.all([p1, p2, p3]) bekommt ein Array von Promises und gibt dir ein einzelnes Promise zurück, das mit einem Array aller aufgelösten Werte erfüllt wird – aber erst, wenn wirklich jedes Promise im Array fulfilled ist. Sobald eines rejected, schlägt das Ganze sofort fehl. Wenn du alle Ergebnisse haben willst, egal ob einzelne fehlschlagen, nimm stattdessen Promise.allSettled.
Promises oder async/await – was nehmen?
Unter der Haube ist das dieselbe Maschinerie – async/await ist im Grunde nur Syntaxzucker über Promises. Neuer Code liest sich mit async/await meistens angenehmer, aber du gibst weiterhin Promises zurück, fängst Fehler mit try/catch oder .catch() ab und greifst für parallele Aufrufe nach wie vor zu Promise.all. Wer Promises verstanden hat, versteht auch async/await.