JavaScript-Tutorial

Mit Promises asynchron programmieren

30.04.2024
Von 
Matthew Tyson ist Java-Entwickler und schreibt unter anderem für unsere US-Schwesterpublikation Infoworld.com.
Promises in JavaScript ermöglichen asynchrone Prozesse in Web- und Server-seitigen Applikationen. Ein Tutorial.
Promises sind ein wichtiger und nützlicher Aspekt von JavaScript, der bei zahlreichen asynchronen Programmier-Tasks unterstützen kann.
Promises sind ein wichtiger und nützlicher Aspekt von JavaScript, der bei zahlreichen asynchronen Programmier-Tasks unterstützen kann.
Foto: CobraCZ | shutterstock.com

Promises stellen in JavaScript einen zentralen Mechanismus dar, um asynchronen Code zu händeln. Sie sind Bestandteil diverser Bibliotheken und Frameworks und werden genutzt, um die Resultate einer Aktion zu managen. Die fetch()-API ist ein Beispiel für Promises im Praxiseinsatz.

Selbst wenn Sie als Entwickler nicht damit vertraut sind, Promises außerhalb eines bestehenden Produkts zu erstellen und zu nutzen, gestaltet es sich überraschend simpel, das zu erlernen. Das trägt nicht nur zu einem besseren Verständnis darüber bei, wie Promises von Bibliotheken genutzt werden, sondern gibt Ihnen auch das richtige Tool für asynchrone Programmieraufgaben an die Hand. Die in diesem Tutorial behandelten Elemente sind ausschließlich High-Level-Komponenten.

Asynchron programmieren mit JavaScript Promises

Im folgenden Beispiel nutzen wir ein Promise, um die Ergebnisse einer Netzwerkoperation zu verarbeiten. Statt eines Netzwerk-Calls verwenden wir einfach einen Timeout:

function fetchData() {

return new Promise((resolve, reject) => {

setTimeout(() => {

const data = "This is the fetched data!";

resolve(data);

}, 2000);

});

}

const promise = fetchData();

promise.then((data) => {

console.log("This will print second:", data);

});

console.log("This will print first.");

In diesem Beispiel definieren wir eine Funktion fetchData(), die ein Promise zurückgibt. Wir callen die Methode und speichern das Promise in der entsprechenden Variable. Anschließend werden die Resultate mit der Promise.then()-Methode verarbeitet. Die Essenz dieses Beispiels: Der fetchData()-Call erfolgt unmittelbar im Code-Fluss - der an then() übergebene Callback hingegen erst, wenn die asynchrone Operation abgeschlossen ist.

Beim Blick auf fetchData() wird klar, dass es ein Promise-Objekt definiert. Dieses Objekt nimmt eine Funktion mit zwei Argumenten an: resolve und reject. Ein erfolgreiches Promise wird resolve aufrufen - falls es ein Problem gibt, geht der Call an reject. In unserem Beispiel simulieren wir das Ergebnis eines Netzwerk-Calls, indem wir resolve aufrufen und einen String zurückgeben.

In vielen Fällen wird ein Promise aufgerufen und direkt verarbeitet. Zum Bespiel folgendermaßen:

fetchData().then((data) => {

console.log("This will print second:", data);

});

Wir betrachten nun den Fehler als Aspekt. In unserem Beispiel können wir eine Fehlerbedingung simulieren:

function fetchData() {

return new Promise((resolve, reject) => {

setTimeout(() => {

if (Math.random() < 0.5) {

reject("An error occurred while fetching data!");

} else {

const data = "This is the fetched data!";

resolve(data);

}

}, 2000);

});

}

In etwa der Hälfte der Fälle wird das Promise in diesem Code in Form eines reject()-Calls fehlschlagen. In der Praxis könnte es dazu kommen, wenn der Netzwerk-Call fehlschlägt oder der Server einen Fehler zurückgibt. Um Fehler beim Aufruf von fetchData() zu händeln, nutzen wir catch():

fetchData().then((data) => {

console.log("That was a good one:", data);

}).catch((error) => {

console.log("That was an error:", error)

});

Wenn Sie diesen Code mehrere Male ausführen, erhalten Sie einen Mix aus Fehlern und Erfolgen. Alles in allem ein simpler Weg, um asynchrones Verhalten zu beschreiben und zu konsumieren.

Promise Chains in JavaScript

Ein wesentlicher Benefit von Promises in JavaScript: Sie lassen sich zu einer Kette verknüpfen. Das hilft dabei, verschachtelte Callbacks zu verhindern und vereinfacht asycnhrones Error Handling.

Wir bleiben bei unserem fetchData()-Funktionsbeispiel - und ergänzen dieses um eine processData()-Funktion. Die hängt wiederum von den Resultaten der fetchData()-Funktion ab. Wir könnten nun die Verarbeitungslogik innerhalb des Return Calls von fetchData() einbetten. Allerdings erlauben Promises ein deutlich saubereres Vorgehen:

function processData(data) {

return new Promise((resolve, reject) => {

setTimeout(() => {

const processedData = data + " - Processed";

resolve(processedData);

}, 1000);

});

}

fetchData()

.then((data) => {

console.log("Fetched data:", data);

return processData(data);

})

.then((processedData) => {

console.log("Processed data:", processedData);

})

.catch((error) => {

console.error("Error:", error);

});

Wenn Sie diesen Code mehrmals ausführen, werden Sie feststellen, dass fetchData() im Erfolgsfall beide then()-Methoden korrekt aufruft. Schlägt die Funktion hingegen fehl, wird die gesamte Promise Chain "kurzgeschlossen" - und das abschließende catch() aufgerufen. Das funktioniert ganz ähnlich wie try/catch-Blöcke.

Würden Sie catch() bereits nach dem ersten then() einfügen, wäre ersteres nur für fetchData()-Fehler zuständig. In unserem Beispiel wird catch() sowohl die fetchData()- als auch die processData()-Fehler behandeln. Der Schlüssel hierzu: Der then()-Handler von fetchData() gibt das Promise von processData(data) zurück. Dadurch können sie miteinander verkettet werden.

5 Wege, JavaScript Promises zu nutzen

Promise.finally()

Ebenso wie try/catch ein finally() erzeugt, wird Promise.finally() unabhängig davon ausgeführt, was in der Promise Chain vonstattengeht:

fetchData()

.then((data) => {

console.log("Fetched data:", data);

return processData(data);

})

.then((processedData) => {

console.log("Processed data:", processedData);

})

.catch((error) => {

console.error("Error:", error);

})

.finally(() => {

console.log("Cleaning up.");

})

Das ist vor allem dann nützlich, wenn Sie etwas zwingend erledigen müssen, etwa eine Connection schließen.

Promise.all()

Im nächsten Szenario nehmen wir an, mehrere Calls parallel tätigen zu müssen. Genauer gesagt handelt es sich um zwei Network Requests, deren Ergebnisse benötigt werden. Wenn eine der beiden Anfragen fehlschlägt, scheitert auch die gesamte Operation. Für diesen Fall könnte der obige Promise-Chain-Ansatz zum Einsatz kommen. Das ist allerdings nicht ideal, da das voraussetzt, dass ein Request abgeschlossen wird, bevor der nächste beginnt. Deshalb nutzen wir stattdessen Promise.all():

Promise.all([fetchData(), fetchOtherData()])

.then((data) => { // data is an array

console.log("Fetched all data:", data);

})

.catch((error) => {

console.error("An error occurred with Promise.all:", error);

});

Weil JavaScript mit einem einzigen Thread arbeitet, laufen diese Operationen zwar nicht wirklich parallel ab - kommen dem aber sehr nahe: Die JavaScript-Engine ist in der Lage, einen Request zu initiieren und währenddessen einen weiteren zu starten.

Wenn eines der an Promise.all() übergebenen Promises fehlschlägt, wird die gesamte Execution gestoppt und zum bereitgestellten catch() weitergeleitet. In dieser Hinsicht könnte man Promise.all() das Attribut "fail fast" zuschreiben. Darüber hinaus können Sie finally() auch in Kombination mit Promise.all() verwenden. Das wird sich erwartungsgemäß verhalten und in jedem Fall ausgeführt, egal wie das Promises-Set ausfällt.

Mit der then()-Methode erhalten Sie ein Array, bei dem jedes Element dem übergebenen Promise entspricht. Das sieht in etwa folgendermaßen aus:

Promise.all([fetchData(), fetchData2()])

.then((data) => {

console.log("FetchData() = " + data[0] + " fetchMoreData() = " + data[1] );

})

Promise.race()

Es kann vorkommen, dass Sie es mit mehreren asynchronen Tasks zu tun bekommen - aber nur der erste erfolgreich sein muss. Etwa, wenn zwei redundante Services existieren und Sie den schnelleren von beiden verwenden wollen.

Für das nachfolgende Beispiel nehmen wir an, dass fetchData() und fetchSameData() zwei Möglichkeiten darstellen, identische Informationen anzufordern - und beide geben Promises zurück. An dieser Stelle können Sie race() einsetzen, um das zu managen:

Promise.race([fetchData(), fetchSameData()])

.then((data) => {

console.log("First data received:", data);

});

Das führt in der Konsequenz dazu, dass der then()-Callback nur einen Rückgabewert für Daten erhält - und zwar den des "siegreichen" (schnellsten) Promise.

Fehler werden bei race() leicht nuanciert: Wenn das zurückgewiesene Promise als erstes auftritt, endet das "race" und es folgt ein Call an catch(). Wenn das zurückgewiesene Promise auftritt, nachdem ein anderes aufgelöst wurde, wird der Fehler ignoriert.

Promise.allSettled()

Wenn Sie warten möchten, bis eine Collection asynchroner Operationen abgeschlossen ist, (unabhängig davon, ob sie fehlschlagen oder erfolgreich sind) können Sie dazu allSettled() verwenden. Zum Beispiel wie folgt:

Promise.allSettled([fetchData(), fetchMoreData()]).then((results) =>

results.forEach((result) => console.log(result.status)),

);

Das results-Argument, das an den then()-Handler übergeben wird, enthält ein Array, das die Ergebnisse der Operationen beschreibt:

[0: {status: 'fulfilled', value: "This is the fetched data!"},

1: {status: 'rejected', reason: undefined}]

Sie erhalten also ein Statusfeld, das entweder fulfilled (resolved) oder rejected wird. In erstgenanntem Fall enthält der Wert das von resolve() aufgerufene Argument. Im Fall abgelehnter Promises wird das reason-Feld mit der Fehlerursache befüllt (insofern eine solche angegeben wurde).

Promise.withResolvers()

Die Spezifikationen zu ECMAScript 2024 enthalten eine statische Promise-Methode namens withResolvers(). Diese wird bereits von den meisten Webbrowsern und Server-seitigen Umgebungen unterstützt. Die neue Methode erlaubt es, ein Promise zusammen mit den resolve- und reject-Funktionen als unabhängige Variablen zu deklarieren und sie dabei im selben Scope zu halten. (fm)

Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.