So verwenden Sie Promises

Promises sind die Grundlage der asynchronen Programmierung in modernem JavaScript. Ein Promise ist ein von einer asynchronen Funktion zurückgegebenes Objekt, das den aktuellen Zustand der Operation darstellt. Zu dem Zeitpunkt, zu dem das Promise dem Aufrufer zurückgegeben wird, ist die Operation oft noch nicht abgeschlossen, aber das Promise-Objekt bietet Methoden, um den späteren Erfolg oder das Scheitern der Operation zu behandeln.

Voraussetzungen: Ein solides Verständnis der JavaScript-Grundlagen und der asynchronen Konzepte, die in den vorherigen Lektionen dieses Moduls behandelt wurden.
Lernziele:
  • Die Konzepte und Grundlagen der Verwendung von Promises in JavaScript.
  • Verketten und Kombinieren von Promises.
  • Fehlerbehandlung in Promises.
  • async und await: wie sie sich auf Promises beziehen und warum sie nützlich sind.

Im vorherigen Artikel haben wir über die Verwendung von Callbacks gesprochen, um asynchrone Funktionen zu implementieren. Bei diesem Design rufen Sie die asynchrone Funktion auf und übergeben Ihre Callback-Funktion. Die Funktion gibt sofort zurück und ruft Ihr Callback auf, wenn die Operation abgeschlossen ist.

Mit einer Promise-basierten API startet die asynchrone Funktion die Operation und gibt ein Promise-Objekt zurück. Sie können dann Handler an dieses Promise-Objekt anhängen, die ausgeführt werden, wenn die Operation erfolgreich war oder fehlgeschlagen ist.

Verwendung der fetch() API

Hinweis: In diesem Artikel werden wir Promises untersuchen, indem wir Codebeispiele von der Seite in die JavaScript-Konsole Ihres Browsers kopieren. Um dies einzurichten:

  1. öffnen Sie einen Browser-Tab und besuchen Sie https://example.org
  2. öffnen Sie in diesem Tab die JavaScript-Konsole in den Entwicklerwerkzeugen Ihres Browsers
  3. wenn wir ein Beispiel zeigen, kopieren Sie es in die Konsole. Sie müssen die Seite jedes Mal neu laden, wenn Sie ein neues Beispiel eingeben, oder die Konsole beschwert sich, dass Sie fetchPromise erneut deklariert haben.

In diesem Beispiel laden wir die JSON-Datei von https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json herunter und protokollieren einige Informationen darüber.

Dazu werden wir eine HTTP-Anfrage an den Server stellen. Bei einer HTTP-Anfrage senden wir eine Anfragenachricht an einen entfernten Server, und dieser sendet uns eine Antwort zurück. In diesem Fall senden wir eine Anfrage, um eine JSON-Datei vom Server zu erhalten. Erinnern Sie sich an den letzten Artikel, wo wir HTTP-Anfragen mit der XMLHttpRequest API gemacht haben? Nun, in diesem Artikel verwenden wir die fetch() API, die moderne, Promise-basierte Ersatzlösung für XMLHttpRequest.

Kopieren Sie dies in die JavaScript-Konsole Ihres Browsers:

js
const fetchPromise = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);

console.log(fetchPromise);

fetchPromise.then((response) => {
  console.log(`Received response: ${response.status}`);
});

console.log("Started request…");

Hier sind wir:

  1. wir rufen die fetch() API auf und weisen den Rückgabewert der Variable fetchPromise zu
  2. loggen die Variable fetchPromise direkt danach. Dies sollte etwas wie Promise { <state>: "pending" } ausgeben, was uns sagt, dass wir ein Promise-Objekt haben und dass es einen state hat, dessen Wert "pending" ist. Der "pending"-Zustand bedeutet, dass die Fetch-Operation noch läuft.
  3. geben eine Handler-Funktion in die then()-Methode des Promise. Wenn (und falls) die Fetch-Operation erfolgreich ist, wird das Promise unseren Handler aufrufen und ein Response-Objekt übergeben, das die Antwort des Servers enthält.
  4. loggen eine Nachricht, dass wir die Anfrage gestartet haben.

Die vollständige Ausgabe sollte in etwa so aussehen:

Promise { <state>: "pending" }
Started request…
Received response: 200

Beachten Sie, dass Started request… protokolliert wird, bevor wir die Antwort erhalten. Im Gegensatz zu einer synchronen Funktion gibt fetch() zurück, während die Anfrage noch läuft, und ermöglicht unserem Programm somit, reaktionsfähig zu bleiben. Die Antwort zeigt den 200- (OK) Statuscode, was bedeutet, dass unsere Anfrage erfolgreich war.

Dies scheint wahrscheinlich sehr ähnlich wie das Beispiel im letzten Artikel, wo wir Event-Handler zum XMLHttpRequest Objekt hinzugefügt haben. Stattdessen übergeben wir einen Handler an die then()-Methode des zurückgegebenen Promise.

Verkettung von Promises

Mit der fetch() API, sobald Sie ein Response-Objekt erhalten, müssen Sie eine andere Funktion aufrufen, um die Antwortdaten zu erhalten. In diesem Fall möchten wir die Antwortdaten als JSON erhalten, also würden wir die Methode json() des Response-Objekts aufrufen. Es stellt sich heraus, dass json() ebenfalls asynchron ist. Dies ist also ein Fall, in dem wir zwei aufeinanderfolgende asynchrone Funktionen aufrufen müssen.

Versuchen Sie dies:

js
const fetchPromise = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);

fetchPromise.then((response) => {
  const jsonPromise = response.json();
  jsonPromise.then((data) => {
    console.log(data[0].name);
  });
});

In diesem Beispiel fügen wir, wie zuvor, einen then()-Handler zu dem von fetch() zurückgegebenen Promise hinzu. Aber dieses Mal ruft unser Handler response.json() auf und gibt dann einen neuen then()-Handler in das von response.json() zurückgegebene Promise ein.

Dies sollte "baked beans" (den Namen des ersten Produkts in "products.json") ausgeben.

Aber Moment mal! Erinnern Sie sich an den letzten Artikel, wo wir gesagt haben, dass wir, indem wir einen Callback in einen anderen Callback aufrufen, immer mehr verschachtelte Ebenen von Code erhalten haben? Und wir sagten, dass dieses "Callback-Problem" unseren Code schwer verständlich machte? Ist dies nicht dasselbe, nur mit then()-Aufrufen?

Es ist, natürlich. Aber das elegante Merkmal von Promises ist, dass then() selbst ein Promise zurückgibt, das mit dem Ergebnis der Funktion, die ihm übergeben wurde, abgeschlossen wird. Dies bedeutet, dass wir (und sollten) den obigen Code so umschreiben können:

js
const fetchPromise = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);

fetchPromise
  .then((response) => response.json())
  .then((data) => {
    console.log(data[0].name);
  });

Anstatt das zweite then() im Handler für das erste then() aufzurufen, können wir das von json() zurückgegebene Promise zurückgeben und das zweite then() darauf aufrufen. Dies nennt man Promise-Verkettung und bedeutet, dass wir immer komplexere Verschachtelungsebenen vermeiden können, wenn wir aufeinanderfolgende asynchrone Funktionsaufrufe machen müssen.

Bevor wir zum nächsten Schritt übergehen, gibt es noch ein weiteres Teil hinzuzufügen. Wir müssen überprüfen, ob der Server die Anfrage akzeptiert hat und in der Lage war, sie zu verarbeiten, bevor wir versuchen, sie zu lesen. Dies tun wir, indem wir den Statuscode der Antwort überprüfen und einen Fehler auslösen, wenn er nicht "OK" ist:

js
const fetchPromise = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);

fetchPromise
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    console.log(data[0].name);
  });

Fehler abfangen

Das bringt uns zum letzten Punkt: Wie behandeln wir Fehler? Die fetch() API kann aus vielen Gründen einen Fehler auslösen (zum Beispiel, weil keine Netzwerkverbindung bestand oder die URL in irgendeiner Weise fehlerhaft war) und wir lösen selbst einen Fehler aus, wenn der Server einen Fehler zurückgegeben hat.

Im letzten Artikel haben wir gesehen, dass die Fehlerbehandlung sehr schwierig sein kann mit verschachtelten Callbacks, da wir Fehler auf jeder Verschachtelungsebene behandeln mussten.

Zur Unterstützung der Fehlerbehandlung bieten Promise-Objekte eine catch()-Methode. Diese ist sehr ähnlich zu then(): Sie rufen sie auf und übergeben eine Handler-Funktion. Während der Handler, der then() übergeben wird, allerdings aufgerufen wird, wenn die asynchrone Operation erfolgreich ist, wird der Handler, der catch() übergeben wird, aufgerufen, wenn die asynchrone Operation fehlschlägt.

Wenn Sie catch() am Ende einer Promise-Kette hinzufügen, wird es aufgerufen, wenn eine der asynchronen Funktionsaufrufe fehlschlägt. So können Sie eine Operation als mehrere aufeinanderfolgende asynchrone Funktionsaufrufe implementieren und einen einzigen Ort haben, um alle Fehler zu behandeln.

Versuchen Sie diese Version unseres fetch()-Codes. Wir haben einen Fehler-Handler mit catch() hinzugefügt und auch die URL so verändert, dass die Anfrage fehlschlägt.

js
const fetchPromise = fetch(
  "bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);

fetchPromise
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    console.log(data[0].name);
  })
  .catch((error) => {
    console.error(`Could not get products: ${error}`);
  });

Versuchen Sie, diese Version auszuführen: Sie sollten den Fehler sehen, der von unserem catch()-Handler protokolliert wird.

Promise-Terminologie

Promises kommen mit einigen sehr spezifischen Begriffen, die es wert sind, dass man sich darüber im Klaren ist.

Erstens kann ein Promise in einem von drei Zuständen sein:

  • pending: Das Promise wurde erstellt, und die asynchrone Funktion, mit der es verbunden ist, war noch nicht erfolgreich oder ist noch nicht gescheitert. Dies ist der Zustand, in dem sich Ihr Promise befindet, wenn es aus einem Aufruf von fetch() zurückgegeben wird und die Anfrage noch läuft.
  • fulfilled: Die asynchrone Funktion war erfolgreich. Wenn ein Promise erfüllt ist, wird dessen then()-Handler aufgerufen.
  • rejected: Die asynchrone Funktion ist gescheitert. Wenn ein Promise abgelehnt wird, wird dessen catch()-Handler aufgerufen.

Beachten Sie, dass was "erfolgreich" oder "fehlgeschlagen" bedeutet, ist hier von der jeweiligen API abhängig. Zum Beispiel lehnt fetch() das zurückgegebene Promise ab, wenn (unter anderem) ein Netzwerkfehler das Senden der Anfrage verhindert hat, erfüllt aber das Promise, wenn der Server eine Antwort sendet, selbst wenn die Antwort ein Fehler wie 404 Nicht gefunden war.

Manchmal verwenden wir den Begriff settled um sowohl fulfilled als auch rejected abzudecken.

Ein Promise ist resolved, wenn es abgeschlossen ist oder wenn es "festgelegt" wurde, um dem Zustand eines anderen Promise zu folgen.

Der Artikel Let's talk about how to talk about promises bietet eine großartige Erklärung der Details dieser Terminologie.

Kombinieren mehrerer Promises

Die Promise-Kette ist das, was Sie benötigen, wenn Ihre Operation aus mehreren asynchronen Funktionen besteht und Sie benötigen, dass jede abgeschlossen ist, bevor die nächste beginnt. Aber es gibt auch andere Möglichkeiten, asynchrone Funktionsaufrufe zu kombinieren, und die Promise API bietet einige Helfer dafür.

Manchmal benötigen Sie, dass alle Promises erfüllt sind, aber sie hängen nicht voneinander ab. In einem solchen Fall ist es viel effizienter, sie alle zusammen zu starten und dann benachrichtigt zu werden, wenn sie alle erfüllt sind. Die Methode Promise.all() ist hier, was Sie brauchen. Es nimmt ein Array von Promises und gibt ein einzelnes Promise zurück.

Das von Promise.all() zurückgegebene Promise ist:

  • erfüllt, wenn und falls alle Promises im Array erfüllt sind. In diesem Fall wird der then()-Handler mit einem Array aller Antworten aufgerufen, in der gleichen Reihenfolge, in der die Promises in all() übergeben wurden.
  • abgelehnt, wenn und falls irgendeins der Promises im Array abgelehnt wird. In diesem Fall wird der catch()-Handler mit dem Fehler aufgerufen, der von dem Promise geworfen wurde, das abgelehnt wurde.

Zum Beispiel:

js
const fetchPromise1 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
  "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);

Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
  .then((responses) => {
    for (const response of responses) {
      console.log(`${response.url}: ${response.status}`);
    }
  })
  .catch((error) => {
    console.error(`Failed to fetch: ${error}`);
  });

Hier machen wir drei fetch()-Anfragen an drei verschiedene URLs. Wenn sie alle erfolgreich sind, loggen wir den Antwortstatus jedes einzelnen. Wenn einer von ihnen fehlschlägt, loggen wir den Fehler.

Mit den angegebenen URLs sollten alle Anfragen erfüllt werden, obwohl der Server für die zweite 404 (Nicht gefunden) anstelle von 200 (OK) zurückgeben wird, weil die angeforderte Datei nicht existiert. Das Ergebnis sollte also so aussehen:

https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json: 200
https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found: 404
https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json: 200

Wenn wir denselben Code mit einer fehlerhaft geformten URL versuchen, wie dieser:

js
const fetchPromise1 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
  "bad-scheme://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);

Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
  .then((responses) => {
    for (const response of responses) {
      console.log(`${response.url}: ${response.status}`);
    }
  })
  .catch((error) => {
    console.error(`Failed to fetch: ${error}`);
  });

Dann können wir erwarten, dass der catch()-Handler ausgeführt wird, und wir sollten etwas wie das sehen:

Failed to fetch: TypeError: Failed to fetch

Manchmal benötigen Sie, dass irgendeins von einer Reihe von Promises erfüllt wird und Ihnen ist egal, welches. In diesem Fall möchten Sie Promise.any(). Dies ist wie Promise.all(), außer dass es erfüllt ist, sobald ein beliebiges von den Promises im Array erfüllt ist, oder abgelehnt wird, wenn sie alle abgelehnt werden:

js
const fetchPromise1 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
  "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);

Promise.any([fetchPromise1, fetchPromise2, fetchPromise3])
  .then((response) => {
    console.log(`${response.url}: ${response.status}`);
  })
  .catch((error) => {
    console.error(`Failed to fetch: ${error}`);
  });

Beachten Sie, dass wir in diesem Fall nicht vorhersagen können, welche Fetch-Anfrage zuerst abgeschlossen wird.

Dies sind nur zwei der zusätzlichen Promise-Funktionen zum Kombinieren mehrerer Promises. Um mehr über die anderen zu erfahren, lesen Sie die Promise Referenzdokumentation.

async und await

Das Schlüsselwort async bietet Ihnen eine einfachere Möglichkeit, mit asynchronem, Promise-basiertem Code zu arbeiten. Indem Sie async am Anfang einer Funktion hinzufügen, machen Sie sie zu einer async-Funktion:

js
async function myFunction() {
  // This is an async function
}

Innerhalb einer async-Funktion können Sie das await-Schlüsselwort vor einem Aufruf einer Funktion, die ein Promise zurückgibt, verwenden. Dies lässt den Code an diesem Punkt warten, bis das Promise abgeschlossen ist, wobei der erfüllt Wert des Promise als Rückgabewert behandelt wird, oder der abgelehnte Wert geworfen wird.

Dies ermöglicht es Ihnen, Code zu schreiben, der asynchrone Funktionen verwendet, aber wie synchroner Code aussieht. Zum Beispiel könnten wir es verwenden, um unser Fetch-Beispiel umzuschreiben:

js
async function fetchProducts() {
  try {
    // after this line, our function will wait for the `fetch()` call to be settled
    // the `fetch()` call will either return a Response or throw an error
    const response = await fetch(
      "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
    );
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    // after this line, our function will wait for the `response.json()` call to be settled
    // the `response.json()` call will either return the parsed JSON object or throw an error
    const data = await response.json();
    console.log(data[0].name);
  } catch (error) {
    console.error(`Could not get products: ${error}`);
  }
}

fetchProducts();

Hier rufen wir await fetch() auf, und anstatt ein Promise zu erhalten, erhält unser Aufrufer ein vollständig abgeschlossenes Response-Objekt zurück, als ob fetch() eine synchrone Funktion wäre!

Wir können sogar einen try...catch Block für die Fehlerbehandlung verwenden, genau wie wir es tun würden, wenn der Code synchron wäre.

Beachten Sie jedoch, dass async-Funktionen immer ein Promise zurückgeben, sodass Sie so etwas nicht tun können:

js
async function fetchProducts() {
  try {
    const response = await fetch(
      "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
    );
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(`Could not get products: ${error}`);
  }
}

const promise = fetchProducts();
console.log(promise[0].name); // "promise" is a Promise object, so this will not work

Stattdessen müssten Sie etwas wie dies tun:

js
async function fetchProducts() {
  const response = await fetch(
    "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
  );
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  const data = await response.json();
  return data;
}

const promise = fetchProducts();
promise
  .then((data) => {
    console.log(data[0].name);
  })
  .catch((error) => {
    console.error(`Could not get products: ${error}`);
  });

Hier haben wir den try...catch wieder in den catch-Handler auf das zurückgegebene Promise verschoben. Dies bedeutet, dass unser then-Handler nicht mit dem Fall umgehen muss, in dem ein Fehler in der fetchProducts-Funktion abgefangen wurde, was dazu führte, dass data undefined war. Behandeln Sie Fehler als letzten Schritt Ihrer Promise-Kette.

Beachten Sie auch, dass Sie await nur innerhalb einer async-Funktion verwenden können, es sei denn, Ihr Code befindet sich in einem JavaScript-Modul. Das bedeutet, dass Sie dies nicht in einem normalen Skript tun können:

js
try {
  // using await outside an async function is only allowed in a module
  const response = await fetch(
    "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
  );
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  const data = await response.json();
  console.log(data[0].name);
} catch (error) {
  console.error(`Could not get products: ${error}`);
  throw error;
}

Sie werden wahrscheinlich häufig async-Funktionen verwenden, wo Sie ansonsten Promise-Ketten verwenden würden, und sie machen die Arbeit mit Promises viel intuitiver.

Denken Sie daran, dass genauso wie eine Promise-Kette, await erzwingt, dass asynchrone Operationen in Serie abgeschlossen werden. Dies ist notwendig, wenn das Ergebnis des nächsten Vorgangs vom Ergebnis des letzten abhängt, aber wenn dies nicht der Fall ist, wird etwas wie Promise.all() leistungsfähiger sein.

Zusammenfassung

Promises sind die Grundlage der asynchronen Programmierung in modernem JavaScript. Sie erleichtern das Ausdrücken und Verstehen von Folgen asynchroner Operationen ohne tief verschachtelte Callbacks und unterstützen eine Art der Fehlerbehandlung, die der synchronen try...catch-Anweisung ähnelt.

Die Schlüsselwörter async und await erleichtern es, eine Operation aus einer Reihe von aufeinanderfolgenden asynchronen Funktionsaufrufen zu erstellen, ohne dass explizite Promise-Ketten erstellt werden, und Sie können Code schreiben, der aussieht wie synchroner Code.

Promises funktionieren in den neuesten Versionen aller modernen Browser; der einzige Ort, an dem die Unterstützung für Promises problematisch sein wird, ist in Opera Mini und IE11 und früheren Versionen.

Wir haben in diesem Artikel nicht alle Funktionen von Promises behandelt, nur die interessantesten und nützlichsten. Wenn Sie anfangen, mehr über Promises zu lernen, werden Ihnen mehr Funktionen und Techniken begegnen.

Viele moderne Web-APIs basieren auf Promises, einschließlich WebRTC, Web Audio API, Media Capture and Streams API und viele mehr.

Siehe auch