async/await & Promises
Asynchrones Programmieren in Node: Promises, async/await, Parallelitaet mit Promise.all und Error-Handling. Die Grundlage fuer jeden Node-Code.
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:
asyncvor der Funktion - sie gibt automatisch ein Promise zurueckawaitblockt nur innerhalb dieser Funktion - nicht den ganzen Event Loopawaitgeht nur inasync-Funktionen (oder seit Top-Level-awaitauch 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/awaitist lesbarer Zucker fuer Promisestry/catchrund umawaitfuer Fehlerbehandlung- Parallel mit
Promise.all, tolerant mitPromise.allSettled forEach+ async funktioniert nicht -for-ofodermap + Promise.all- Immer
awaitsetzen, sonst floating promises - Top-Level-await in ESM-Modulen
Im naechsten Kapitel: Express-Server und REST-APIs bauen.