JavaScript Promises

Asynchrone Programmierung mit Promises ist einfacher als gedacht, versprochen!

Wie funktioniert asynchrone Programmierung in JavaScript und wie verwendet man dafür Promises?

Intro: Callback Hölle

Ältere APIs arbeiten Callback-basiert: Man übergibt eine Funktion welche aufgrufen wird, sobald die aysnchrone Operation abgeschlossen wurde (entweder mit Error oder einem Ergebnis).


login(username, pw, (user, error) => {
  if (user) {
    getRoles(user.id, (roles, error) => {
      if (roles) {
        updateLastLoggedInDate(user, (error) => {
          if (!error) {
            redirectToDashboard();
          } else {
            handleError();
          }
        });
      } else {
        handleError(error);
      }
    });
  } else {
    handleError(error);
  }
});

Was ist ein Promise?

Ein Objekt, welches asynchron zum Programmablauf Daten zur Verfügung
stellt oder einen Fehler liefert.

Promise Status

Ein Promise kann immer nur einen diese drei Status haben:

Pending: Promise, welches noch keinen Wert oder Fehler hat.

Fulfilled: Promise war erfolgreich

Rejected: Promise hat Fehler verursacht

Wie erstellt man ein Promise?


const promise = new Promise((resolve, reject) => {
  // success
  resolve('ok');
  // error
  reject(error);
});

// registering resolve-callback
promise.then(result => {
  console.log('result', result);
});

// registering error-callback
promise.catch(error => {
  console.log('promise error', error);
});

Jedes Promise beginnt nach der Erstellung sofort mit seiner Arbeit.

then/catch

Mit then übergibt man eine Funktion die aufgerufen wird, wenn das Promise erfolgreich ausgeführt wird oder bereits erfolgreich ausgeführt wurde.

Man kann auch öfters then aufrufen.

catch ist der Error-Handler. Dieser wird aufgerufen wenn ein Fehler passiert.

Achtung

Was nicht geht ist resolve() oder reject() öfters aufzurufen.

Ein Promise kann nur von Pending -> Fulfilled oder Pending -> Rejected überführt werden. Danach ist das Promise abgeschlossen.

Chaining / finally

Statt eine "Callback Pyramide of Doom" zu bauen sollte man Promise-Chaining nutzen. Ein Ergebnis eines Promises wird als Input zum nächsten Promise weitergereicht.

Jeder Return-Wert im then steht als neues Promise zur Verfügung:


// login returns a promise which returns a remote user 
// when log in succeeds
const loginPromise = login(username, pw);

const userNamePromise = loginPromise.then(user => {
  return user.username;
});

userNamePromise.then(name => {
  console.log('username is:', name);
});

Diese Promises können nun verkettet werden, kurz chaining:


login(username, pw)
  .then(user => {
    return user.username;
  })
  .then(name => {
    console.log('username is:', name);
  });

Zusätzlich kann ein Error-Handler angehängt werden, der im Fehlerfall aufgerufen wird:


login(username, pw)
  .then(user => {
    return user.username;
  })
  .then(name => {
    console.log('username is:', name);
  })
  .catch(error => {
    console.log('Oops! Error: ', error)
  });

Mit finally kann man einen Handler definieren, der immer am Ende der Promise-Chain aufgerufen wird, auch im Fehlerfall:


login(username, pw)
  .then(user => {
    return user.username;
  })
  .then(name => {
    console.log('username is:', name);
  })
  .catch(error => {
    console.log('Oops! Error: ', error)
  })
  .finally(() => console.log('Done!'));

Hier ein Beispiel wie man Daten mittels Fetch-API abruft und miteinander verbindet:


// loading all posts
fetch("https://jsonplaceholder.typicode.com/posts")
  .then(response => {
    // converting response-body to json
    return response.json();
  })
  .then(posts => {
    // getting all userIds from posts
    return posts.map(post => post.userId);
  })
  .then(userIds => {
    // trick to remove duplicates from array and picking first 3
    return Array.from(new Set(userIds)).slice(0, 3);
  })
  .then(uniqIds => {
    // making new requests from id-array:
    // promise all loads all desired promises in parallel
    // returning all results at once as array.
    return Promise.all(
      uniqIds.map(id =>
        fetch("https://jsonplaceholder.typicode.com/users/" + id)
          .then(response => response.json())
      )
    );
  })
  .then(users => {
    // accessing the resulting users:
    console.log('3 users who wrote posts: ', users);
  })
  .catch(error => {
    console.log('Something went wrong:', error);
  });

Error Handling

Passiert ein Fehler wird der nächstliegende Error-Handler aufgerufen und die dazwischenliegenden Promises übersprungen:


loadA()
  .then(result => {
    // error occurs here. division by zero
    const someValue = result / 0;
    return loadB(someValue);
  })
  .then(result => loadC(result))
  .catch(error => {
    console.log("Oh no! Error:", error);
  })
  .then(() => loadFoobar())
  .catch(error => {
    console.log("Foobar loading failed! Error:", error);
  })
  .finally(() => {
    proceedToDashboard();
  });

.then(result => loadC(result)) wird nie aufgerufen, weil der nächste catch-Handler in der Chain aufgerufen wird.

Danach wird die Chain aber weiter abgearbeitet, wenn nach dem catch weitere then folgen.

finally wird immer aufgerufen, jedoch ohne Übergabewert.

Promise.resolve(value)

Funkion welche sofort ein Promise im Status fulfilled erzeugt mit dem gewünschten Wert:


Promise.resolve('Hello World!')
  .then(value => {
    console.log('promise resolved to:', value);
  });

Promise.reject(reason)

Funkion welche sofort ein Promise im Status rejected erzeugt mit dem gewünschten Grund/Fehler:


Promise.reject('NO NO NO!')
  .then(value => {
    // never called
  })
  .catch(error => {
    console.log('Reason:', error);
  });

Promise.all([promises])

Funktion welche alle übergebenen Promises gleichzeitig ausführt und alle Ergebnisse in der gleichen Reihenfolge als Array dem nächsten then-Handler übergibt:


Promise.all([loadA(), loadB(), loadC()]).then(results => {
  console.log("A", results[0]);
  console.log("B", results[1]);
  console.log("C", results[2]);
});

Im Fehlerfall gilt die gesamte Operation als fehlerhaft, nach dem Motto "Alles oder nichts".

Promise.allSettled([promises])

Ähnlich wie Promise.all nur dass ein fehler eines einzelnen Promises nicht zum Abbruch führt.

Die Ergebnise sind mit Meta-Informationen ausgestattet, welche angeben, ob das jeweilige Promise resolved oder rejected wurde.

Promise.race([promises])

Führt alle Promises gleichzeitig aus, das schnellste Promise gewinnt. Das Ergebnis wird dem nächsten then-Handler übergeben. Die Ergebnisse der anderen Promises werden ignoriert.

Für alle die noch mehr über Promises wissen wollen

Hier fast eine Stunde Material mit ausführlichen Beispielen und Erklärungen:

Auf Youtube ansehen

Links:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://flaviocopes.com/javascript-event-loop
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
https://jsonplaceholder.typicode.com/
http://bluebirdjs.com/docs/api/cancellation.html
https://rxjs.dev/

Kommentare

comments powered by Disqus