mitmario.dev

Promises

JavaScript Im Browser 4 Min Lesezeit 3 BeispieleLektion 4 von 8

Ein Promise ist ein Platzhalter für ein Ergebnis, das es noch nicht gibt.

Der Vergleich, der überall benutzt wird, ist die Nummer an der Wursttheke. Du bekommst sie sofort, und sie ist nicht die Wurst. Sie ist die Zusage, dass du irgendwann drankommst. Vielleicht auch die Nachricht, dass es aus ist.

Genau das macht den Unterschied zum Callback aus. Beim Callback gibst du deine Anweisungen mit, bevor du weißt, ob überhaupt etwas kommt. Beim Promise bekommst du ein Ding zurück, das du weiterreichen, in einer Variablen ablegen und an mehreren Stellen abfragen kannst.

Die drei Zustände

Ein Promise ist in genau einem von drei Zuständen:

  • pending, also schwebend. So fängt jedes an.
  • fulfilled, also erfüllt. Es hat einen Wert.
  • rejected, also abgelehnt. Es hat einen Grund, üblicherweise ein Error-Objekt.

Der Wechsel passiert genau einmal. Ein erfülltes Promise wird nie wieder abgelehnt, ein abgelehntes nie wieder erfüllt, und ein zweiter Aufruf von aufloesen bleibt wirkungslos. Das ist eine der wichtigsten Eigenschaften: Du musst dich nie fragen, ob dein Handler zweimal läuft.

then, catch, finally

then, catch und finally
const lager = { a1: "Tastatur", b2: "Maus" };

function artikelLaden(id) {
  return new Promise((aufloesen, ablehnen) => {
    setTimeout(() => {
      const name = lager[id];
      if (!name) {
        ablehnen(new Error(`Nicht im Lager: ${id}`));
        return;
      }
      aufloesen(name);
    }, 150);
  });
}

artikelLaden("a1")
  .then((name) => {
    console.log("Gefunden:", name);
  })
  .catch((fehler) => {
    console.error("Ging nicht:", fehler.message);
  })
  .finally(() => {
    console.log("Anfrage 1 ist durch, so oder so.");
  });

artikelLaden("zzz")
  .then((name) => {
    console.log("Gefunden:", name);
  })
  .catch((fehler) => {
    console.error("Ging nicht:", fehler.message);
  })
  .finally(() => {
    console.log("Anfrage 2 ist durch, so oder so.");
  });

console.log("Beide Anfragen sind abgeschickt.");

An ein Promise hängst du Funktionen, die aufgerufen werden, sobald es soweit ist:

  • then(fn) läuft, wenn es erfüllt wurde. fn bekommt den Wert.
  • catch(fn) läuft, wenn es abgelehnt wurde. fn bekommt den Grund.
  • finally(fn) läuft in beiden Fällen und bekommt nichts. Dasselbe Aufräum-Werkzeug wie in Lektion 14.2.

Die drei sehen aus wie try, catch und finally, und das ist volle Absicht. Nur dass sie hier auch dann funktionieren, wenn zwischen dem Aufruf und dem Ergebnis eine halbe Sekunde liegt.

Beachte im Beispiel, wo Beide Anfragen sind abgeschickt. in der Console steht: ganz oben. Der Code darüber hat nur angemeldet, nicht gewartet.

Verketten

Das vergessene return im then
function zahlLaden() {
  return Promise.resolve(10);
}

// Richtig: jedes then gibt etwas zurück, und das kommt im
// nächsten an.
zahlLaden()
  .then((zahl) => {
    return zahl * 2;
  })
  .then((verdoppelt) => {
    console.log("mit return:", verdoppelt);
  });

// Falsch: das then rechnet zwar, gibt aber nichts zurück.
// Im nächsten kommt deshalb undefined an.
zahlLaden()
  .then((zahl) => {
    zahl * 2;
  })
  .then((verdoppelt) => {
    console.log("ohne return:", verdoppelt);
  });

Jetzt kommt der Teil, der Promises von Callbacks unterscheidet: Jedes then gibt selbst wieder ein Promise zurück.

Deshalb darfst du direkt das nächste then daranhängen. Und was du in einem then zurückgibst, kommt im nächsten als Wert an. Aus der Verschachtelung wird eine Liste, die untereinander steht.

Gibst du in einem then selbst ein Promise zurück, wird darauf gewartet, und das nächste then bekommt dessen aufgelösten Wert. Genau damit lassen sich Schritte aneinanderreihen, die alle Zeit brauchen.

Der häufigste Fehler dabei ist ein vergessenes return. Im zweiten Beispiel steht in der zweiten Kette zahl * 2; ohne return. Gerechnet wird, das Ergebnis fällt auf den Boden, und im nächsten then kommt undefined an. Wenn in einer Kette plötzlich undefined auftaucht, sieh im then davor nach.

Im Reiter Debug stehen die beiden Ketten direkt untereinander, und der Unterschied ist eine einzige Zeile: verdoppelt=20 in der einen, verdoppelt=undefined in der anderen. Zwei Schritte davor steht der Grund, nämlich einmal return zahl * 2; und einmal zahl * 2;.

Mit einer Pfeilfunktion ohne geschweifte Klammern passiert das nicht: .then((zahl) => zahl * 2) gibt automatisch zurück. Das ist einer der Gründe, warum man diese Schreibweise in Ketten so oft sieht. Sie hat allerdings einen Nebeneffekt, der dir gleich auffallen wird: Sie taucht im Protokoll gar nicht auf. Aufgezeichnet werden Anweisungen, und eine Pfeilfunktion ohne Klammern hat keine, sie ist nur ein Ausdruck.

Ein catch für die ganze Kette

Ein Fehler wandert die Kette entlang
function schritt(marke, scheitert) {
  return new Promise((aufloesen, ablehnen) => {
    setTimeout(() => {
      console.log("schritt läuft:", marke);
      if (scheitert) {
        ablehnen(new Error(`abgebrochen bei ${marke}`));
        return;
      }
      aufloesen(marke);
    }, 80);
  });
}

schritt("a", false)
  .then((a) => schritt("b", true))
  .then((b) => schritt("c", false))
  .then((c) => {
    console.log("Alle drei durch:", c);
  })
  .catch((fehler) => {
    console.error("Ein catch für alle drei:", fehler.message);
  });

// Und hier ohne catch. Sieh dir an, was in der Console steht.
schritt("x", true);

Das ist die zweite große Verbesserung gegenüber Callbacks.

Wird irgendwo in der Kette abgelehnt, überspringt die Ausführung alle folgenden then und läuft zum ersten catch. Ein einziges catch am Ende deckt damit jeden Schritt davor ab.

Im dritten Beispiel scheitert Schritt b. In der Console siehst du, dass schritt läuft: c nie erscheint: Der dritte Schritt wird gar nicht erst gestartet.

Nach einem catch geht die Kette übrigens weiter, als wäre nichts gewesen. Ein then hinter dem catch läuft also wieder, und das ist genau richtig, wenn das catch einen Ersatzwert liefert.

Unbehandelte Ablehnungen

Ganz unten im dritten Beispiel steht ein Aufruf ohne catch. Sieh dir an, was im Reiter Console passiert.

Ein abgelehntes Promise, an dem niemand ein catch hängen hat, wird als unhandled rejection gemeldet. Bei uns steht dann Unhandled Promise Rejection: Error: abgebrochen bei x da, in den Entwicklerwerkzeugen deines Browsers heißt dieselbe Zeile Uncaught (in promise). Gemeint ist beides Mal: Hier hat etwas abgelehnt, und niemand hat zugehört.

Das ist wichtig zu wissen, weil es der einzige Hinweis ist, den du bekommst. Das Programm bricht nicht ab, es läuft einfach ohne das Ergebnis weiter. Wenn eine Seite still etwas nicht tut, ist ein Blick in die Console der erste Griff.

Selbst eines bauen

new Promise((aufloesen, ablehnen) => { ... }) baut ein eigenes. Die Funktion, die du hineingibst, läuft sofort, und darin rufst du irgendwann eines von beiden auf.

Das brauchst du seltener, als es aussieht. Fast alles, was heute asynchron ist, gibt schon ein Promise zurück: fetch in Abschnitt 16, die meisten Browser-Funktionen, jede halbwegs aktuelle Bibliothek. Selbst bauen musst du nur, wenn du etwas Altes umhüllst, das noch mit Callbacks arbeitet, oder wenn du eine Wartezeit brauchst.

Zwei Abkürzungen für den Alltag: Promise.resolve(wert) gibt ein bereits erfülltes Promise, Promise.reject(fehler) ein bereits abgelehntes. Beides ist praktisch in Funktionen, die manchmal etwas laden müssen und manchmal schon alles wissen.

Zum Mitnehmen

Ein Promise ist ein Platzhalter für ein Ergebnis, das es noch nicht gibt. Es wechselt seinen Zustand genau einmal, und danach nie wieder.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.