mitmario.dev

Promises in Node

Node.js Sandbox 3 Min Lesezeit 4 BeispieleLektion 3 von 7

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.

Der Callback-Stil, Fehler zuerst
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

Was ein ignorierter Fehler anrichtet
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

Dasselbe mit await
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.

util.promisify als Brücke
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, 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.

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.