Abschnitt 6 · Lektion 3
Promises in Node
Node ist älter als Promises. Als die Standardbibliothek entstand, gab es async und await noch
nicht, und deshalb sieht sie an vielen Stellen zweimal so aus, wie sie aussieht.
Fast jedes eingebaute Modul gibt es in zwei Ausführungen: node:fs arbeitet mit Rückrufen,
node:fs/promises mit Promises. Dasselbe bei node:dns, node:timers und node:stream. Du hast
in Abschnitt 3 durchgehend die Promise-Variante benutzt, ohne dass wir viel Aufhebens darum gemacht
hätten. Jetzt schauen wir uns die andere an, denn lesen können musst du sie.
Der Callback-Stil und seine feste Form
Im alten Stil bekommt eine Funktion als letztes Argument eine Funktion mit. Die wird aufgerufen, wenn das Ergebnis da ist.
Milch kaufen
Kurs weiterbauen
Rechnung schreiben
Blumen giessen import { readFile } from "node:fs";
readFile("notizen.txt", "utf8", (fehler, text) => {
if (fehler) {
console.error(`Konnte nicht lesen: ${fehler.code}`);
return;
}
console.log(`${text.trim().split("\n").length} Zeilen`);
}); Wichtig ist die Reihenfolge der Parameter: Der Fehler kommt zuerst. Ist er null, hat es
geklappt, und die Nutzdaten stehen im zweiten Parameter. Ist er gesetzt, gibt es keine Nutzdaten.
Das nennt sich Fehler-zuerst-Verabredung, und das Wort Verabredung ist wörtlich gemeint: Es ist keine Sprachregel, sondern eine Gewohnheit, auf die sich alle geeinigt haben. Die Sprache prüft nichts. Wenn du den ersten Parameter nicht anschaust, sagt dir niemand etwas.
Und genau das ist die Falle
import { readFile } from "node:fs";
readFile("gibtsnicht.txt", "utf8", (fehler, text) => {
console.log(`Inhalt: ${text}`);
console.log("Fertig.");
}); Die Datei gibt es nicht. Der Fehler steht sauber im ersten Parameter, wird aber nie angeschaut, und
der zweite ist undefined. Das Programm läuft weiter, gibt Unsinn aus und beendet sich mit
Exit-Code 0. Alles in Ordnung, sagt es.
Das ist der teuerste Unterschied zwischen den beiden Stilen. Ein await ohne try wirft, und
ein geworfener Fehler ist laut. Ein Callback ohne if (fehler) ist still.
Derselbe Vorgang mit Promises
Milch kaufen
Kurs weiterbauen
Rechnung schreiben
Blumen giessen import { readFile } from "node:fs/promises";
try {
const text = await readFile("notizen.txt", "utf8");
console.log(`${text.trim().split("\n").length} Zeilen`);
} catch (fehler) {
console.error(`Konnte nicht lesen: ${fehler.code}`);
} Kürzer ist es kaum, und das ist auch nicht der Punkt. Der Punkt ist, dass der Fehlerweg jetzt der
normale Fehlerweg der Sprache ist: try und catch, so wie überall sonst. Du kannst mehrere
Aufrufe in einen try legen, du kannst Fehler nach oben durchreichen, und du kannst nicht
versehentlich vergessen hinzuschauen, denn dann bricht es ab.
Beim Fangen lohnt sich ein zweiter Blick auf den Fehler. Ein catch, das jeden Fehler gleich
behandelt, verschluckt auch die, mit denen du nicht gerechnet hast. fehler.code sagt dir, was
wirklich los war: ENOENT heißt „gibt es nicht”, EACCES heißt „darfst du nicht”. Das kennst du aus
Lektion 3.5, und hier ist es genauso wichtig.
Faustregel: Neuer Code nimmt die Promise-Variante. Den Callback-Stil schreibst du nicht mehr, du liest ihn nur noch.
Wenn eine fremde Bibliothek noch Callbacks nimmt
Das kommt vor, und dann willst du sie nicht in deinem ganzen Code herumtragen. util.promisify
macht aus einer Funktion mit Fehler-zuerst-Callback eine, die ein Promise zurückgibt.
import { promisify } from "node:util";
const altmodisch = (name, weiter) => {
setTimeout(() => weiter(null, `Hallo ${name}`), 10);
};
const modern = promisify(altmodisch);
console.log(await modern("Mario")); Eine Zeile, und ab da benutzt du sie wie alles andere. Bedingung ist nur, dass die Funktion sich an die Verabredung hält: Callback als letztes Argument, Fehler an erster Stelle.
Einen laufenden Vorgang abbrechen
Bleibt noch ein Baustein, den du kennen solltest, auch wenn er hier nur kurz vorkommt.
Ein Promise hat keinen Abbruchknopf. Wenn du eine Anfrage gestartet hast und sie dauert zu lange,
kannst du nicht einfach sagen „lass es”. Dafür gibt es AbortController: Du erzeugst einen, gibst
sein signal an den Aufruf mit, und ein controller.abort() beendet den Vorgang.
Fast alle eingebauten Funktionen, die warten können, nehmen so ein Signal entgegen, fetch
eingeschlossen. Der abgebrochene Vorgang scheitert dann mit einem Fehler namens AbortError, und den
fängst du wie jeden anderen.
Wir kommen in Lektion 15.5 darauf zurück, wenn es darum geht, dass ein Server nicht ewig auf eine fremde Schnittstelle warten darf. Für den Moment reicht es zu wissen, dass es das gibt und wie es heißt.
Zum Mitnehmen
Fast jedes eingebaute Modul gibt es zweimal: mit Callbacks und mit Promises. Beim Callback-Stil steht der Fehler immer an erster Stelle.
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.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.