Das Promise-Objekt wurde in JavaScript mit ES6 eingeführt. Mit Promises lassen sich asynchrone Operationen in JavaScript handhaben. In diesem Artikel geht es um:
- Hintergrund zu Promises
- Was sind Promises?
- Logik von Promises visualisiert
- Zustände und Methoden von Promises
- Syntax im Detail
Hintergrund zu Promises
Wie in meinem Beitrag “Event-Loop: Wie JavaScript arbeitet” beschrieben, hat JavaScript eine Single-Thread-Beschaffenheit. Es kann jeweils nur eine Aufgabe ausgeführt werden. Code wird also synchron (sequentiell) ausgeführt. Doch zum Glück gibt es Web APIs wie setTimeout, die es ermöglichen, dass Code asynchron abgelaufen werden kann. Auch können eigene Callback-Funktionen geschrieben werden, damit Code asynchron ablaufen kann.
Das Problem an Callbacks ist jedoch, dass Code ineinander verschachtelt wird. Das kann zu unlesbaren Code führen, der sehr schwer nachzuvollziehen ist. Dieses Problem ist in JavaScript als “Callback Hell” (Callback-Hölle) bekannt:
doSomething(function(response) {
doSomethingElse(response, function(nextResponse) {
doThirdThing(nextResponse, function(finalResponse) {
console.log('Final result: ' + finalResponse);
}, failureCallback);
}, failureCallback);
}, failureCallback);
Um das Problem der Unübersichtlichkeit zu lösen, sollen Promises mit ES6 etwas Abhilfe schaffen. Damit sollen Callbacks lesbarer erstellt werden können:
firstRequest()
.then(function(response) {
return secondRequest(response);
}).then(function(nextResponse) {
return thirdRequest(nextResponse);
}).then(function(finalResponse) {
console.log('Final response: ' + finalResponse);
}).catch(failureCallback);
An den Code-Beispiel wird auch der Unterschied zwischen Callbacks und Promises ersichtlich: Während bei Callbacks Funktionen in Funktionen übergeben werden, ist der Weg bei Promises ein anderer. Hier werden Callbacks angehängt. Es werden also weiterhin Callbacks benutzt. Nur erfolgt hier das sogenannte “Chaining”.
Was sind Promises?
Vereinfacht ausgedrückt sind Promises wie im echten Leben “Versprechen”. Macht man ein Versprechen, dann verdeutlicht man, dass etwas in Zukunft garantiert stattfinden wird. 2 mögliche Szenarien, können dann in Zukunft auftreten: Das Versprechen wird entweder gehalten oder auch nicht. Ähnlich funktionieren Promises in JavaScript. Wenn die Zeit gekommen ist, wird das Versprechen entweder eingelöst (fulfilled) oder es wird nicht eingehalten (rejected).
Aus JS-Sicht sind Promises Objekte, mit denen man asynchrone Operationen steuern kann. Es ist ein “Versprechen”, das etwas später ausgeführt wird. Es bezieht sich also auf die Zukunft. Wird ein Promise erzeugt, werden Parameter an die Konstruktorfunktion übergeben. Diese Parameter spiegeln den Zustand des Promises wieder. An den Promise werden dann Methoden mitgegeben, die die jeweiligen Zustände abfangen. Wie das konkret aussieht, sehen wir uns nachfolgend an:
Logik von Promises visualisiert
Am besten kann man Promises verstehen, wenn man die Logik visualisiert:
Zustände und Methoden von Promises
Wie man an der Grafik sehen kann, kann ein Promise-Objekt eines von drei Zustände haben:
- Pending (Schwebend): Das ist der Anfangszustand, noch bevor etwas passiert ist.
- Fulfilled (Erfüllt): Tritt ein, wenn die Operation erfolgreich abgeschlossen wurde und das Versprechen einen aufgelösten Wert besitzt.
- Rejected (Zurückgewiesen): Tritt ein, wenn die Operation fehlgeschlagen ist. Das Versprechen wurde also nicht eingelöst und gibt meist einen Fehler zurück.
Wenn wir Daten vom Server anfragen und dabei Promises einsetzen, dann befindet sich das Promise in “pending” solange es auf eine Antwort wartet. Werden die Daten korrekt zurückgegeben, dann war alles erfolgreich (“fulfilled”). Werden die Daten nicht übermittelt, dann hätten wir den Zustand “rejected”. Wenn man sich die Grafik oben genauer anschaut, dann fallen außerdem folgende Prozesse auf:
- Pending -> Resolve -> Resolved -> Fullfilled
- Pending -> Reject -> Rejected -> Rejected
Am Anfang ist das Promise schwebend. Im Promise wird dann eine Kondition geprüft (Details dazu weiter unten) und es kann wie folgt weiter gehen:
- Ist die Kondition “wahr”, dann beginnt der resolve-Prozess und das Promise wird nach Abschluss auf “resolved” gesetzt. Wenn der Wert aufgelöst wurde, befindet sich der Zustand auf fulfilled (erfüllt). Darauf wird dann die Funktion then() angewendet.
- Ist die Kondition “falsch”, dann beginnt der reject-Prozess und das Promise wird nach Abschluss auf “rejected” gesetzt. Wenn der Wert nicht aufgelöst wurde, befindet sich der Zustand auf “rejected” (zurückgewiesen). Darauf wird dann die Funktion catch() angewendet.
Der endgültige Zustand (egal ob fullfilled oder rejected) wird dann nochmal als “settled” bezeichnet.
Syntax im Detail
Um ein Promise zu erstellen, wird zunächst der Konstruktor ausgeführt. Dies führt dazu, dass das Promise-Objekt erstellt wird:
const myPromise = new Promise();
Der Kontruktor new Promise() nimmt im nächsten Schritt einen Parameter entgegen. Der Parameter wird als Steuerfunktion (Executor Function) bezeichnet:
const myPromise = new Promise(executorFunction);
Die Funktion muss jedoch vorher definiert worden sein:
const executorFunction = (resolve, reject) => {};
const myPromise = new Promise(executorFunction);
Eine andere Schreibweise wäre es, dass man die Steuerfunktion direkt als Parameter mitgibt:
const myPromise = new Promise((resolve, reject) => {
// Hier kommt die Kondition
});
Wie man sehen kann werden die 2 Funktionen resolve und reject als Parameter übergeben. Dies sind 2 Funktionen, direkt von JavaScript vorgegeben:
- Wenn resolve aufgerufen wird, dann ändert diese Funktion den Status des Promises von pending auf fulfilled. Das Promise bekommt dann den Wert aus der resolve()-Funktion.
- Wenn reject aufgerufen wird, dann ändert diese Funktion den Status des Promises von pending auf rejected. Das Promise bekommt dann den Wert aus der reject()-Funktion.
Was noch fehlt ist die Logik innerhalb des Promises, welche ausgeführt werden soll:
const myPromise = new Promise((resolve, reject) => {
let condition;
if(condition is met) {
resolve('Promise is resolved successfully.');
} else {
reject('Promise is rejected');
}
});
Je nachdem welcher Zustand eintritt, kann darauf reagiert werden. Das Prinzip lautet: “Wenn das Promise den Zustand settled annimmt, dann sollte etwas passieren”. Hier kommen die Methoden then() und catch() zum Einsatz:
- then() wird ausgeführt, wenn das Versprechen eingehalten worden ist (fulfilled).
- catch() wird ausgeführt, wenn das Vesprechen nicht eingehalten worden ist (rejected).
Wenn also das Versprechen eingelöst wurde, wird die Methode then() ausgeführt:
myPromise.then((message) => {
console.log(message);
});
Andersrum – also bei reject – wird dann die Methode catch() aufgerufen:
myPromise.then((message) => {
console.log(message);
}).catch((message) => {
console.log(message);
});
Callbacks vs. Promises
Um den Unterschied zwischen Callbacks und Promises nochmal zu verdeutlichen, sehen wir uns weitere Code-Beispiele an. Wie man sehen wird, werden an das Promise Callback-Funktionen angehängt anstatt sie zu übergeben.
Zunächst einmal ein klassisches Callback-Beispiel. Es werden 2 Funktionen erstellt, die dann an eine andere Funktion übergeben werden:
function success(result) {
console.log("Erfolgreich generiert: " + result);
}
function failure(error) {
console.error("Fehlerhaft generiert: " + error);
}
createFile(settings, success, failure);
Mit Promises können die Callback-Funktionen direkt angehangen werden. Die letzte Zeile könnte wie folgt umgeschrieben werden:
createFile(settings).then(success, failure);
Oder auch als Lang-Schreibweise:
const promise = createFile(settings);
promise.then(success, failure);