Promises

Wie bereits zuvor erwähnt sind Promises ein Versprechen, dass etwas in er Zukunft fertig sein wird (Funktion).

In heutzutage modernen Browsern ist jede lang-laufende Operation hinter einem solchen Promise versteckt.

Promises sorgen dafür das eine Funktion (Message) in die Queue gesteckt wird und das der Event-Loop diese später ausführt.

Ein Promise zu erstellen ist relativ simpel


      const myPromise = new Promise();
    

Dieses Promise macht nun aber nicht viel. Ein Promise benötigt eine Funktion die im Hintergrund ausgeführt wird.


      const myPromise = new Promise(() => {
        console.log('Hallo Welt!');
      });
    

Wir sollten beim ausführen dann ein "Hallo Welt!" in den Developer Tools sehen

Wie sieht das nun aber alles aus wenn wir schauen wollen in welcher Reihenfolge das alles ausgeführt wird?


      const myPromise = new Promise(() => {
        console.log('Hallo Welt!');
      });

      console.log('Komme ich davor oder danach?');
    

Ein Promise macht auch nicht mehr als den Callback auszuführen und auf ein Resultat zu warten.

Wenn es nichts gibt auf das man warten muss, ist ein Promise auch sofort abgeschlossen.

Bevor wir weiter machen, gehen wir auf den Callback des Promises näher ein.

Dieser Callback erhält nämlich 2 Parameter, die wiederrum Callbacks sind


      const p = new Promise((resolve, reject) => {
        // ???
      });
    

Oft werden diese 2 Callbacks als resolve und reject definiert.

Schauen wir mal, was die Variable als Inhalt hat, in der wir dieses Promise packt:


      const myPromise = new Promise(() => {
        console.log('Hallo Welt!');
      });

      // Promise { <state>: "pending" }
      console.log(myPromise);
    

Das Promise hat einen Zustand der in "pending" hängt.

Dieser Zustand wird sich in dieser Form so auch nicht mehr ändern.

Hierfür sind die Callback-Funktionen resolve & reject gedacht.

Rufen wir mal die Funktion resolve innerhalb des Promises auf


      const myPromise = new Promise((resolve) => {
        console.log('Hallo Welt!');
        resolve();
      });

      // Promise { <state>: "fulfilled", <value>: undefined }
      console.log(myPromise);
    

Die Promise ist nun fulfilled und scheint einen value zu beinhalten, der in diesem Fall undefined ist.

Sehen wir uns noch kurz an, wenn der Promise rejected wird


      const myPromise = new Promise((_, reject) => {
        console.log('Hallo Welt!');
        reject();
      });

      // Promise { <state>: "rejected", <value>: undefined }
      console.log(myPromise);
    

Gut, wir wissen nun dass der Promise "rejected" wurde, aber was heißt das genau?

Nun, Promises funktionieren ein wenig anders als einfach aufgerufene Funktionen.

Wenn wir die Variable myPromise untersuchen, sehen wir keine Möglichkeit, auf den Zustand oder den Wert zuzugreifen.

Aber es gibt ein paar Funktionen: then, catch & finally.

Wir können nur auf Resultate und den Allgemeinen Ausgang eines Promise reagieren indem wir mit diesen Funktionen arbeiten.

Diese Funktionen sind chainable. Wir können mehrere .then & .catch nacheinander aufrufen.

Wenn ein Promise erfolgreich ist und resolve aufruft, wird die Funktion in .then(() => {}) aufgerufen

Sollte das Promise nicht erfolgreich gewesen sein und hat reject() aufgerufen, so wird .catch(() => {}) aufgerufen.

Wir können nicht aus einem Promise "ausbrechen". Wir können aber Resultate aus einem Promise zu jederzeit in den äußeren Scope verschieben und weitere Funktionen aufrufen.

Sehen wir uns das ganze nun in Aktion an:


      const myPromise = new Promise((resolve, reject) => {
        resolve();
      });

      myPromise.then(() => {
        console.log('Das war erfolgreich!');
      });
    

Der Fehlerfall ist auch leicht:


      const myPromise = new Promise((resolve, reject) => {
        reject();
      });

      myPromise.catch(() => {
        console.log('Das war diesmal NICHT erfolgreich!');
      });
    

Führen wir beide Varianten einmal zusammen:


      const myPromise = new Promise((resolve, reject) => {
        reject();
      });

      myPromise
      .then(() => {
        console.log('Das ging mal gut!');
      })
      .catch(() => {
        // Durch das reject landen wir nur hier!
        console.log('Das war diesmal NICHT erfolgreich!');
      });
    

Das catch muss aber nicht das Ende sein, wenn wir nach dem catch noch etwas ausführen wollen, können wir einfach mit .then weiter machen.


      const myPromise = new Promise((resolve, reject) => {
        reject();
      });

      myPromise
      .then(() => {
        console.log('Das ging mal gut!');
      })
      .catch(() => {
        console.log('Das war diesmal NICHT erfolgreich!');
      })
      .then(() => {
        console.log('Es gibt mehr danach!');
      });
    

Das weitere .then und .catch aufgerufen werden können, hat einen entscheidenden Vorteil:

Wir können in .then und .catch einen Fehler werfen um ein anderes .catch auszulösen.

Gleichzeitig bedeutet ein nicht vorhanden sein eines Errors, dass das nächste .then aufgerufen wird.


      const myPromise = new Promise((resolve, reject) => {
        resolve();
      });

      myPromise
      .then(() => {
        console.log('Das ging mal gut!');
        throw new Error('Pech gehabt!');
      })
      .catch(() => {
        console.log('Das war diesmal NICHT erfolgreich!');
      })
      .then(() => {
        console.log('Es gibt mehr danach!');
      });
    

Um am Ende auf jeden Fall eine Funktion auszuführen gibt es zu guter letzt noch die Funktion .finally.

Der Aufruf sorgt dafür, dass in jedem Fall - egal ob ein Fehler geworfen wurde oder nicht - Code ausgeführt wird.


      const myPromise = new Promise((resolve, reject) => {
        resolve();
      });

      myPromise
      .then(() => {
        console.log('Das ging mal gut!');
      })
      .catch(() => {
        console.log('Das war diesmal NICHT erfolgreich!');
      })
      .finally(() => {
        console.log('Ich geschehe immer!');
      });
    

Parameter in .catch und .then

Wir erhalten in diesen beiden Funktionen Informationen.

.catch erhält in der Regel den Error der davor geworfen wurde, oder aber was in die Funktion reject mitgegeben wurde.


      const p = new Promise((_, reject) => {
        reject('Zum Scheitern verdonnert');
      });

      p.catch((reason) => {
        console.log(reason);
        throw new Error('Für immer');
      })
      .catch((err) => {
        console.log(err);
      });
    

Das gleiche Funktioniert auch mit resolve() und .then()


      const p = new Promise((resolve) => {
        resolve(42);
      });

      p.then((zahl) => {
        console.log(zahl);
        return "Die einzig wahre Zahl";
      })
      .then((x) => {
        console.log(x);
      });
    

Ein Konkretes Beispiel

Nachfolgend lesen wir den Inhalt aus dem System-Clipboard aus. Die Funktionalität, die uns der Browser dafür gibt, liefert einen Promise.


      const into = document.getElementById('clipboard-result');
      document.getElementById('clipboard-paste').onclick = function () {
        navigator.clipboard.readText()
        .then((clipboardText) => {
          into.innerText = `Kopierter Text:\n${clipboardText}`;
        })
        .catch((err) => {
          into.innerText = `Fehler beim lesen des Clipboards: ${err}`;
        });
      }
    
Ergebnis...