Promise - das Versprechen

November 2024
Wir beißen uns gar nicht an dem Wort Promise (übersetzt: Versprechen) fest und sagen gleich, worum es geht; nämlich um das Starten eines Teilprogramms in einem eigenen Thread. Wir betrachten gleich mal den Code des folgenden Beispiels und schauen uns an, was dabei passiert.

Das Beispiel

Das folgende Beispiel ist absichtlich ähnlich denen, wie man sie im Internet findet, wenn man nach Promise Example sucht. Dabei war ein Beispiel von freeCodeCamp Inspiration.

HTML

<button onclick="location.reload()">Seite neu laden</button>
<h3>Promise Example</h3>

<h4>Display:</h4>
<div id="display"></div>

CSS

h3, h4 {
  margin-top:4pt;
  margin-bottom:4pt;
}
#display {
  padding:4pt;
  border: 1pt solid red;
  width: min-content;
  white-space: nowrap;
}

JavaScript

// Definitions

const Display = document.getElementById("display");

function Log(parm) { Display.innerHTML += 'Ok : ' + parm + '<br>'; }
function Err(parm) { Display.innerHTML += 'Err: ' + parm + '<br>'; }

const mypromise = new Promise((resolve, reject) => {
  const num = Math.random();
  if (num >= 0.5) {
    resolve(num);
  } else {
    reject(num);
  }
});

// Main

Log('Anfang');
mypromise.then(Log, Err);
Log('Ende');
Wenn wir dieses Beipiel starten, kann sein, dass es auf dem Monitor ähnlich so aussieht:

Screenshot 1

Anzeige einer Zahl hinter Ok:
oder ähnlich so:

Screenshot 2

Anzeige einer Zahl hinter Err:

Erklärung zum Programmablauf

Im HTML-Bereich gibt es ein div mit dem class-Attribut "display". Die Funktion Log() erwartet ein Argument und gibt es mit dem Vorspann Ok: aus. Nach allen Definitionen im Scriptcode wird Log() mit dem String 'Anfang' als erste Anweisung aufgerufen und folglich erscheint im Display als erstes diese Ausgabe.
Als letzte Anweisung wird Log() mit 'Ende' aufgerufen, und auch das sehen wir im Display. Zwischen beiden Aufrufen steht im Scriptcode ein Promise-Aufruf:
promise.then(Log, Err);
Dieser Code produziert eine Zeile im Display mit einer Gleitkommazahl.
Eigentlich müsste zuerst die Zeile mit Anfang, dann die mit der Gleitkommazahl und zuletzt die Zeile mit Ende ausgegeben werden. Aber unsere Screenshots zeigen eine andere Reihenfolge - was geht hier ab?
Das Promise startet seine Ausgabe eben in einem anderen Thread. Das JavaScript-Programm arbeitet so wie erwartet:
  1. erste Ausgabe ausführen
  2. das Promise starten
  3. letzte Ausgabe ausführen - fertig und Schluss
Das geht sehr schnell. Das Promise jedoch arbeitet sein Programm in einem eigenen Tread ab, also asynchron, will heißen zeitlich parallel und unabhängig vom Hauptprogramm. Die Ausführung braucht hier ein paar Nanosekunden länger, daher kommt die Ausgabe des Promise verspätet an.
Das Promise ist also gedacht, dass es Prozesse abarbeiten soll, auf die das eigentliche JavaScript-Programm nicht warten soll; etwas das Nachladen eines Fotos oder Videos von einem Server. Das Promise kann bei Erfolg den passenden Code starten oder im Fehlerfall z.B. eine entsprechende Nachricht ausgeben.
Schauen wir uns nun den Code an, der im Promise steht. Es wird eine zufällige Zahl num von Math.random() initialisiert. Das ist irgendeine Gleitkommazahl zwischen 0 und 1. Ist diese Zahl größer oder gleich 0,5 dann gilt dies als erfolgreich abgearbeitet, und im Display steht vor der Zahl das Ok: (siehe Screenshot 1). Ansonsten wird fehlerhaft ausgeführt vermutet und vor der Zahl steht Err: (Screenshot 2).
Bei fehlerhaft geht es hier nicht um ein fehlerhaft abgelaufenes Programm, sondern um das Ergebnis des beabsichtigten Prozesses. Nicht das Promise stellt fest, ob Erfolg oder nicht, sondern der Prozess selbst muss testen, ob das Ergebnis ein gewünschtes ist oder nicht. Unser Prozess ist im Kern Math.random(), und mit Erfolg haben wir ziemlich willkürlich nur Resultate eingestuft, die nicht kleiner als 0,5 sind. Das Promise hat durchaus nichts dagegen, so etwas zu programmieren.
Wenn wir das Beispielprogramm starten, kann entweder das eine oder andere passieren. Startet man die Seite mehrmals neu (Reload-Button klicken), dann wird mal der eine, mal der andere Fall eintreten.
Soweit die Beschreibung, was hier passiert. Vielleicht bleibt ein Gefühl zurück, das man irgendwas noch nicht ganz verstanden hat. Daher betrachten wir das Beispiel mal ganz genau.

Definition des Promise

const mypromise = new Promise(...);
In dieser Zeile wird ein Promise-Objekt mit Namen mypromise erzeugt (instanziert). Der Konstruktoraufruf erwartet in den Klammern ein Argument, das hier symbolisch mit ... angegeben ist.
Den Code zwischen diesen Klammern betrachten wir mal näher:
(resolve, reject) => {
  const num = Math.random();
  if (num >= 0.5) {
    resolve(num);
  } else {
    reject(num);
  }
}
Es ist eine Funktionsdefinition, das Promise erwartet als Parameter also eine Callbackfunktion.
Die Funktionsdefinition liegt in Pfeilschreibweise vor (arrow function). Der Code wird deutlicher, wenn wir die Funktion konservativ umschreiben; also
  • mit function am Anfang,
  • gefolgt von einem Namen (z.B. myfunction),
  • Parameterliste,
  • und Funktionskörper in geschweiften Klammern.
Die Instanzierung unseres Promise verkürzt sich dabei erheblich, statt der ganzen Funktionsdefinition brauchen wir nur den Funktionsnamen als Parameter anzugeben. Das Ganze sieht dann so aus:
function myfunction(resolve, reject) {
  const num = Math.random();
  if (num >= 0.5) {
    resolve(num);
  } else {
    reject(num);
  }
}
const mypromise = new Promise(myfunction);
Hier das geänderte Beispiel, es funktioniert in gleicher Weise.
Natürlich kann die Funktion auch als anonyme Funktion (closure) geschrieben werden.
let myfunction = function(resolve, reject) {
  const num = Math.random();
  if (num >= 0.5) {
    resolve(num);
  } else {
    reject(num);
  }
}

const mypromise = new Promise(myfunction);
Hier das dritte Beispiel.
Jetzt haben wir die Callbackfunktion extra vorliegen. Hier packen wir also den Code hinein, den das Promise asynchron ausführen soll. Wenn es ein sehr umfangreicheres Programm sein sollte, dann ist die Extra-Schreibweise bestimmt übersichtlicher.

Die Parameter resolve und reject

Die Verwendung der Parameter resolve und reject zeigt, dass es offenbar Funktionsaufrufe sind; hier der betreffende Teilcode:
  ...
  if (num >= 0.5) {
    resolve(num);
  } else {
    reject(num);
  }
  ...
Daraus folgt, dass die Argumente für diese Parameter auch Callbackfunktionen sein müssen.
In unserem Beispiel haben wir dafür die Funktionen Log() und Err() bereitgestellt. Im Erfolgsfall ruft das Promise die erste Funktion auf, andernfalls die zweite. Diese Reihenfolge ist vorgegeben, die Namen der Callbackfunktionen jedoch nicht, die können wir frei wählen. Vergleiche dazu mit Beispielen im MDN zu Promise.
Aber wo genau müssen wir die Funktionen angeben? Das geschieht über die Methode then() des Promises, in unserem Beispiel ist es diese Codezeile:
...
mypromise.then(Log, Err);
...

Es geht auch ohne

Wir könnten ein Promise mit einem Code instanzieren, bei denen der Fehlerfall nicht interessiert oder nicht definiert ist. Dann brauchen wir keine Callbackfunktion für den Fehlerfall, und können sie einfach weglassen, im Beispiel so:
...
mypromise.then(Log);
// oder so:
mypromise.then(Log, null);
...
Unser Beispiel zeigt dann nur den Erfolgsfall an.
Andernfalls könnte es sein, dass uns nur der Fehlerfall interessiert. Dann müssen wir beachten, dass die Callbackfunktion für den Fehlerfall als zweites Argument anzugeben ist, und zwar so:
...
mypromise.then(null, Err);
...
Jetzt zeigt unser Beispiel nur den Fehlerfall an.

Zusammenfassung

  1. Das Promise hat die Aufgabe, einen definierten Code als Prozess in einem eigenen Thread ablaufen zu lassen.
    Hauptprogramm und Thread laufen zeitlich parallel, also unabhängig voneinander. In unserem Beispiel ist das Hauptprogramm eher beendet als der Thread.
  2. Das Promise ist nicht dafür da, um syntaktische oder Runtime-Fehler abzufangen.
    Der Prozess im Thread muss selbst wissen, ob sich bei Abarbeitung ein Erfolg oder Misserfolg einstellt.
  3. Was in dem einen oder anderen Fall zu tun ist, entscheidet das Promise nicht.
    Es startet nur von außen bereitgestellte Callback-Funktionen für den einen oder anderen Fall.

Promise erzeugt Promise

Die Promise-Instanz, im Beispiel mypromise, hat auch einen Rückgabewert, und zwar: ein neues Promise!
Das können wir feststellen, wenn wir den Rückgabewert abfangen und mit alert() anzeigen lassen.
Code, Screenshot, Link zu Beispiel
Welchen Prozess das neue Promise enthält, wissen wir nicht, denn wir haben es ja selbst nicht instanziert. Aber Promise bleibt Promise, also gibt es auch hier die Methode then(), der wir einfach mal Callbackfunktionen für die beiden Fälle übergeben. Zwar können wir hier auch Log() und Err() verwenden, zur Unterscheidung der Ausgaben definieren wird die ähnlichen Funktionen Log2() und Err2().
vollständiger Teilcode, Screenshot, Link zu Beispiel