Zum Inhalt springen
Node.js Fortgeschritten 30 min

async/await & Promises

Asynchrones Programmieren in Node: Promises, async/await, Parallelitaet mit Promise.all und Error-Handling. Die Grundlage fuer jeden Node-Code.

Aktualisiert:
Inhaltsverzeichnis

async/await & Promises

Node ist non-blocking. Statt auf jede Datei, Datenbank oder API zu warten, laeuft dein Code weiter und wird benachrichtigt, wenn das Ergebnis da ist. Das ist der Grund, warum Node so schnell ist - aber es braucht ein gutes Verstaendnis von Promises und async/await.

Das Problem - Callbacks

Frueher sah asynchroner Code so aus:

import fs from "node:fs";

fs.readFile("a.txt", "utf8", (err, data) => {
  if (err) return console.error(err);
  fs.readFile("b.txt", "utf8", (err, data2) => {
    if (err) return console.error(err);
    fs.writeFile("c.txt", data + data2, (err) => {
      if (err) return console.error(err);
      console.log("Fertig!");
    });
  });
});

Das ist die beruehmte Callback-Hoelle - verschachtelt, schwer lesbar, fehleranfaellig.

Promises - die Revolution

Ein Promise ist ein Objekt, das einen zukuenftigen Wert repraesentiert. Es hat drei Zustaende:

  • pending - noch nicht fertig
  • fulfilled - erfolgreich, hat einen Wert
  • rejected - fehlgeschlagen, hat einen Fehler
import { readFile } from "node:fs/promises";

readFile("a.txt", "utf8")
  .then((data) => console.log(data))
  .catch((err) => console.error(err));

readFile liefert jetzt ein Promise statt einen Callback zu nehmen.

Chaining

readFile("a.txt", "utf8")
  .then((data) => data.toUpperCase())
  .then((upper) => writeFile("b.txt", upper))
  .then(() => console.log("Fertig!"))
  .catch((err) => console.error(err));

Der return-Wert in .then() wird das naechste Promise.

async/await - die Syntax

async/await ist syntaktischer Zucker fuer Promises - viel lesbarer:

import { readFile, writeFile } from "node:fs/promises";

async function verarbeite() {
  const data = await readFile("a.txt", "utf8");
  const upper = data.toUpperCase();
  await writeFile("b.txt", upper);
  console.log("Fertig!");
}

verarbeite();

Regeln:

  • async vor der Funktion - sie gibt automatisch ein Promise zurueck
  • await blockt nur innerhalb dieser Funktion - nicht den ganzen Event Loop
  • await geht nur in async-Funktionen (oder seit Top-Level-await auch im Modul-Scope)

Top-Level await

In ESM-Modulen geht await auch ganz oben:

// index.mjs (oder mit "type": "module" in package.json)
import { readFile } from "node:fs/promises";

const inhalt = await readFile("a.txt", "utf8");
console.log(inhalt);

Extrem praktisch fuer Skripts.

Error Handling

Mit try/catch

try {
  const data = await readFile("a.txt", "utf8");
  console.log(data);
} catch (err) {
  console.error("Fehler:", err.message);
}

Mit .catch()

const data = await readFile("a.txt", "utf8").catch((err) => {
  console.error(err);
  return "";
});

Wann was?

  • try/catch: mehrere Zeilen, differenzierte Behandlung
  • .catch(): einzelne Operation, Default-Wert bei Fehler

Parallel mit Promise.all

Mehrere Operationen gleichzeitig:

const [a, b, c] = await Promise.all([
  readFile("a.txt", "utf8"),
  readFile("b.txt", "utf8"),
  readFile("c.txt", "utf8"),
]);

Drei Dateien werden parallel gelesen. Wenn eine scheitert, scheitert das Ganze.

Promise.allSettled - auch bei Fehlern

Wenn du jedes Ergebnis willst - auch die Fehler:

const ergebnisse = await Promise.allSettled([
  fetch("https://api1.com"),
  fetch("https://api2.com"),
  fetch("https://api3.com"),
]);

for (const r of ergebnisse) {
  if (r.status === "fulfilled") {
    console.log("OK:", r.value);
  } else {
    console.error("Fehler:", r.reason);
  }
}

Promise.race - der schnellste gewinnt

const daten = await Promise.race([
  fetch("https://primaer.com"),
  fetch("https://backup.com"),
]);

Oft genutzt fuer Timeouts:

function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timeout")), ms)
  );
}

try {
  const daten = await Promise.race([
    fetch("https://api.example.com/slow"),
    timeout(5000),
  ]);
} catch (err) {
  console.error(err.message);
}

Promises erzeugen

Du kannst eigene Promises bauen:

function warte(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

await warte(1000);
console.log("Eine Sekunde spaeter...");

Praktisch: Promises aus Callback-APIs

import { promisify } from "node:util";
import { exec } from "node:child_process";

const execP = promisify(exec);

const { stdout } = await execP("ls");
console.log(stdout);

promisify wandelt Callback-Style-APIs in Promise-Style um.

Haeufige Fallen

forEach mit async funktioniert nicht

// Schlecht - wartet NICHT
files.forEach(async (file) => {
  await verarbeite(file);
});
console.log("Fertig?");   // wird ausgegeben, bevor die Files fertig sind

Richtig:

// Sequenziell
for (const file of files) {
  await verarbeite(file);
}

// Oder parallel
await Promise.all(files.map((f) => verarbeite(f)));

Fehlendes await

async function save() { /* ... */ }

save();             // FEHLER - Promise wird nicht awaited
await save();       // RICHTIG

TypeScript warnt dich bei โ€œfloating promisesโ€. In JS muss man selber aufpassen.

return await

Meistens redundant, aber nicht in try/catch:

async function a() {
  return fetch("...");      // OK
}

async function b() {
  try {
    return await fetch("..."); // mit try: await noetig, damit catch greift
  } catch (err) {
    // ...
  }
}

Sequentiell vs. Parallel

// Sequentiell (nacheinander - langsam)
const a = await fetch("...");
const b = await fetch("...");
const c = await fetch("...");

// Parallel (gleichzeitig - schnell)
const [a, b, c] = await Promise.all([
  fetch("..."),
  fetch("..."),
  fetch("...")
]);

Bei unabhaengigen Operationen immer parallel.

Mit Concurrency-Limit

Manchmal willst du nicht 1000 parallel, sondern 5 gleichzeitig. Kleine Library: p-limit:

import pLimit from "p-limit";

const limit = pLimit(5);   // max 5 parallel

const urls = [...];  // 1000 URLs
const ergebnisse = await Promise.all(
  urls.map((url) => limit(() => fetch(url)))
);

Praktisches Beispiel

Viele Datei-Zugriffe parallel:

import { readFile } from "node:fs/promises";
import path from "node:path";

async function zeileninAllen(ordner, endung = ".txt") {
  const dateien = await readdir(ordner);
  const relevant = dateien.filter((f) => f.endsWith(endung));

  const inhalte = await Promise.all(
    relevant.map((f) => readFile(path.join(ordner, f), "utf8"))
  );

  const zeilen = inhalte.reduce((sum, text) => sum + text.split("\n").length, 0);
  console.log(`${relevant.length} Dateien, ${zeilen} Zeilen`);
}

await zeileninAllen("./logs");

HTTP-Requests parallel

async function fetchMehrere(urls) {
  const responses = await Promise.all(urls.map((u) => fetch(u)));
  const daten = await Promise.all(responses.map((r) => r.json()));
  return daten;
}

const daten = await fetchMehrere([
  "https://api.github.com/users/vercel",
  "https://api.github.com/users/anthropic",
]);

console.log(daten.map((d) => d.name));

Zusammenfassung

  • Promises repraesentieren zukuenftige Werte
  • async/await ist lesbarer Zucker fuer Promises
  • try/catch rund um await fuer Fehlerbehandlung
  • Parallel mit Promise.all, tolerant mit Promise.allSettled
  • forEach + async funktioniert nicht - for-of oder map + Promise.all
  • Immer await setzen, sonst floating promises
  • Top-Level-await in ESM-Modulen

Im naechsten Kapitel: Express-Server und REST-APIs bauen.

Zurรผck zum Node.js Kurs