Abschnitt 15 · Lektion 4
Promises
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
<p>Zwei Anfragen an dieselbe Funktion, eine geht gut aus.</p> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
p {
margin: 0 0 0.75rem;
} 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.fnbekommt den Wert.catch(fn)läuft, wenn es abgelehnt wurde.fnbekommt 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
<p>Zweimal dieselbe Kette. In der zweiten fehlt ein return.</p> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
p {
margin: 0 0 0.75rem;
} 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
<p>Ein catch am Ende reicht für alle Schritte davor.</p> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
p {
margin: 0 0 0.75rem;
} 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, kostenlosZu 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.