Promise - das Versprechen

November 2024
Die Beiträge sind gedacht für JavaScript-Programmierer, die etwas über Promise wissen möchten. Promise werden häufig im Zusammenhang mit fetch() erklärt, das machen wir in späteren Beiträgen. Hier geht es erst mal nur um das Promise selbst, was es kann und wie genau es funktioniert.
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.

Vorbereitungen

Wir erstellen ein neues HTML-Dokument anhand des Grunddokuments für Beispiele und Demos und bringen folgenden HTML-Code ein:

HTML

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

<h4>Display:</h4>
<div id="display"></div>
Es genügt uns ein Versuchsdokument, das man auch direkt im Browser öffnen kann (Filezugriff) ohne einen Webserver nutzen zu müssen.
Das folgende Beispiel wird gleich beim Laden der Seite ausgeführt. Das Reload-Button ermöglicht uns dadurch, den Scriptcode quasi neu zu starten.
Das div-Element mit id="display" wird unser Display. Es soll Textausgaben sichtbar machen und wird uns von der Notwendigkeit entheben, unbedingt console.log verwenden zu müssen.

CSS

Mit etwas Styling, hier nur ein Vorschlag, soll mindestens das Display gut sichtbar sein, auch wenn es leer ist.
h3, h4 {
  margin-top:4pt;
  margin-bottom:4pt;
}
#display {
  padding:4pt;
  border: 1pt solid red;
  width: min-content;
  white-space: nowrap;
}

JavaScript

Den JavaScript-Code für unseren Versuch bringen wir der Einfachheit halber im Scriptbereich 2 unter. Im Folgenden wird Code ausführlich erklärt.

Und hier kommt der JavaScript-Code

Unser Beispiel ist absichtlich denen ähnlich, die man im Internet findet, wenn man nach Promise Example sucht. Dabei war ein Beispiel von freeCodeCamp Inspiration.
// 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:
Die Zahlen werden zufällig generiert, sobald wir auf Reload klicken, ändert sich die Anzeige.

Der äußere Programmablauf

Die Funktion Log() erwartet ein Argument und gibt es mit dem Vorspann Ok: auf unserem Display 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);
Die Funktionen produzieren 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 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; etwa 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.

Der innere Programmablauf

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.
Typisches Beispiel wäre das Nachladen eines Bildes:
  • Bild kommt an - Prozess erfolgreich.
  • Bild gibt es gar nicht - Prozess nicht erfolgreich
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.
Wenn wir das Beispielprogramm starten, kann also 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.

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, die in in Pfeilschreibweise vorliegt (arrow function). Diese Funktion entscheidet, was als Erfolgs- oder Fehlerfall gilt sowie wann bzw. wo die Funktionen resolve() und reject() aufgerufen werden.
Der Code wird deutlicher, wenn wir die Funktion umschreiben; also
  • mit function am Anfang,
  • gefolgt von einem Namen (z.B. myfunction),
  • Parameterliste,
  • und Funktionskörper in geschweiften Klammern,
also ganz konservativ und anständig.
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);
Die Instanzierung des Promise verkürzt sich dabei erheblich, statt der ganzen Funktionsdefinition brauchen wir nur den Funktionsnamen als Parameter anzugeben. Kurz: Das Promise erwartet bei der Instanzierung eine Callback-Funktion (oder eine Funktionsdefinition, in jedem Fall ein Funktionsobjekt).
Hier das geänderte Beispiel, es funktioniert in gleicher Weise.
Wenn es ein sehr umfangreicheres Programm sein sollte, dann ist die Extra-Schreibweise vielleicht etwas übersichtlicher und besser wartbar.
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);

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 Callback-Funktionen sein müssen.
Diese Callback-Funktionen werden auch als Handler bezeichnet, was vielleicht die bessere Sprechweise ist.
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 Handler 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);
...
Log, Err sind beim Aufruf durch .then() also die Argumente für die Parameter resolve, reject.

Es geht auch ohne

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