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

Ein ähnliches Beispiel von freeCodeCamp war Inspiration für dieses Beispiel.

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 und das Beispiel reagiert genauso - 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 aber arbeitet sein Programm in einem eigenen Tread ab, also zeitlich parallel und unabhängig vom Hauptprogramm. Es braucht halt ein paar Nanosekunden länger, daher kommt die Ausgabe des Promise verspätet.
Das Promise ist also gedacht, dass es Prozesse abarbeiten soll, die etwas länger dauern könnten, auf die aber das 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).
Wenn wir das Beispielprogramm starten, kann entweder das eine oder andere passieren; Seite über Reload-Button ggf. mehrmals herunterladen.
Bei diesem Programm gibt es keinen Runtime-Fehler, es läuft nicht wirklich fehlerhaft. Das Programm definiert selbst, was als erfolgreich abgearbeitet und als fehlerhaft ausgeführt gilt.
Soweit eine Erklärung, was hier passiert, aber den Code mit dem Promise verstehen wir wahrscheinlich noch nicht. Die nächsten Abschnitte klären auf.

Definition des Promise

const mypromise = new Promise(...);
In dieser Zeile wird ein Promise-Objekt mit Namen mypromise erzeugt. Der Konstruktoraufruf erwartet in den Klammern ein Argument, das hier symbolisch mit ... angegeben ist.
Genau das Gleiche passiert auch in unserem Beispiel, nur dass zwischen den Klammern ziemlich viel Code steht, nämlich dieser:
(resolve, reject) => {
  const num = Math.random();
  if (num >= 0.5) {
    resolve(num);
  } else {
    reject(num);
  }
}
Genauer betrachtet erkennt man, dass das eine Funktionsdefinition in Pfeilschreibweise ist. Konservativ programmiert beginnt eine Funktionsdefinition mit function gefolgt von einem Funktionsnamen. Schreiben wir den Code doch mal um; als Funktionsnamen verwenden wir myfunction.
function myfunction(resolve, reject) {
  const num = Math.random();
  if (num >= 0.5) {
    resolve(num);
  } else {
    reject(num);
  }
}
Und die Instanzierung unseres Promise verkürzt sich erheblich:
const mypromise = new Promise(myfunction);
Hier das geänderte Beispiel, es funktioniert.
Programmierer, die von anderen Programmiersprachen kommen, wundern sich wahrscheinlich, dass eine komplette Funktiondefinition wie im ersten Beispiel als Argument fungieren kann. Das liegt an der Besonderheit von JavaScript, dass alles ein Objekt ist, auch eine Funktionsdefinition, myfunction ist nicht nur der Name einer Funktion, sondern quasi auch ein Objekt, das eben eine Funktion enthält. Wenn wir unsere Funktion als anomyme Funktion schreiben und einer Variablen zuweisen, wird das wahrscheinlich deutlicher.
let myfunction = function(resolve, reject) {
  const num = Math.random();
  if (num >= 0.5) {
    resolve(num);
  } else {
    reject(num);
  }
}
Soweit
const mypromise = new Promise(myfunction);

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, die Argumente für diese Parameter müssen auch Callback-Funktionen sein. In unserem Beispiel haben wir dafür die Funktionen Log() und Err() bereitgestellt. 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);
...

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, aber 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.

Ausblick

Damit haben wir die Grundfunktionalität des Promise erklärt, aber noch lange nicht alles gesagt. Es gibt weitere Eigenschaften und Methoden, die die Anwendung von parallel laufenden Prozessen steuern.
Unser Beispiel beleuchtet den Fall, dass wir einen Prozess bereitgestellt haben, der parallel ablaufen soll. Es gibt aber auch Funktionen, z.B. fetch(), die ein Promise als Ergebnis zurückgeben. Da wissen wir nicht im Detail, was intern passiert und benötigen zusätzliche Informationen, um das Ergebnis zu verwerten.