⏳Asynchrones JavaScript

Warten, ohne zu blockieren: Während ein Timer läuft oder eine Anfrage unterwegs ist, bleibt die Seite bedienbar. Die Werkzeuge dafür haben sich entwickelt – von Callbacks über Promises zu async/await.

🪜Callbacks → Promises → async/await

Dreimal dieselbe Aufgabe: zwei Nutzer nacheinander laden. Alle drei Varianten laufen wirklich (im Sandbox-Worker).

1 · Callbacks

1function lade(id, fertig) {
2 setTimeout(() => fertig(null, { id, name: "Nutzer " + id }), 20);
3}
4lade(1, (err, nutzer) => {
5 if (err) return console.log("Fehler", err);
6 console.log("geladen:", nutzer.name);
7 lade(2, (err, zweiter) => { // Verschachtelung wächst …
8 console.log("geladen:", zweiter.name);
9 });
10});
Konsole (echt ausgeführt)
wird ausgeführt …

Jeder weitere Schritt rückt eine Ebene ein („Callback-Hölle“); Fehler muss man überall einzeln prüfen.

2 · Promises

1function lade(id) {
2 return new Promise((resolve) =>
3 setTimeout(() => resolve({ id, name: "Nutzer " + id }), 20));
4}
5lade(1)
6 .then((n) => { console.log("geladen:", n.name); return lade(2); })
7 .then((n) => console.log("geladen:", n.name))
8 .catch((e) => console.log("Fehler", e));
Konsole (echt ausgeführt)
wird ausgeführt …

then gibt ein neues Promise zurück – die Kette bleibt flach, ein catch am Ende fängt alle Fehler.

3 · async/await

1const lade = (id) => new Promise((resolve) =>
2 setTimeout(() => resolve({ id, name: "Nutzer " + id }), 20));
3
4async function main() {
5 try {
6 const n1 = await lade(1);
7 console.log("geladen:", n1.name);
8 const n2 = await lade(2);
9 console.log("geladen:", n2.name);
10 } catch (e) {
11 console.log("Fehler", e);
12 }
13}
14main();
Konsole (echt ausgeführt)
wird ausgeführt …

Liest sich wie synchroner Code, ist aber dieselbe Promise-Mechanik darunter. Fehler fängt ein normales try/catch.

🚦Die drei Zustände eines Promise

pendingfulfilledrejectedresolve(wert)reject(grund) / throwsettled (erledigt)

Ein Promise ist genau einmal erledigt: Nach resolve oder reject ändert sich der Zustand nie wieder – weitere Aufrufe werden ignoriert. Die Callbacks laufen als Microtask, also erst nachdem der aktuelle Code fertig ist.

🏁Promise.all, allSettled, race und any als Zeitleiste

Stelle Dauer und Ausgang der drei Promises ein. Die Entscheidung jedes Kombinators wird berechnet – und kann mit echten Promises gegengeprüft werden.
Promise A
Promise B
Promise C
0 ms375 ms750 ms1125 ms1500 ms
Promise A
Promise B
Promise C
Promise.all
✗ 500 ms
Promise.allSettled
✓ 1200 ms
Promise.race
✗ 500 ms
Promise.any
✓ 800 ms
Promise.all([A, B, C])ES2015

Alle müssen klappen. Erste Ablehnung → sofort abgelehnt.

rejected nach 500 ms: "fehler-b"

B wird nach 500 ms abgelehnt → Promise.all bricht sofort ab („fail fast“). Die übrigen Ergebnisse werden ignoriert.

Promise.allSettled([A, B, C])ES2020

Wartet auf alle, wird immer erfüllt – mit Statusobjekten.

fulfilled nach 1200 ms: [{ status: "fulfilled", value: "a" }, { status: "rejected", reason: "fehler-b" }, { status: "fulfilled", value: "c" }]

Wartet auf alle (1200 ms) und wird immer erfüllt – mit einem Statusobjekt je Eingabe.

Promise.race([A, B, C])ES2015

Das erste erledigte Promise entscheidet – erfüllt oder abgelehnt.

rejected nach 500 ms: "fehler-b"

B ist nach 500 ms als Erstes erledigt – egal ob erfüllt oder abgelehnt.

Promise.any([A, B, C])ES2021

Das erste erfüllte gewinnt. Nur wenn alle scheitern: AggregateError.

fulfilled nach 800 ms: "c"

C ist nach 800 ms das erste erfüllte Promise. Ablehnungen davor werden ignoriert.

📡fetch gegen die Beispiel-API

Die App bringt eine kleine API mit erfundenen Aufgaben mit: /api/beispiele, /api/beispiele/:id, ?verzoegerung=ms, ?fehler=1. Die Anfragen unten sind echt.
1const res = await fetch("/api/beispiele");
2console.log(res.status, res.ok);
3const daten = await res.json(); // zweites await: Body lesen
4console.log(daten.anzahl, daten.beispiele[0].titel);

💡 fetch liefert zuerst nur die Antwort-Kopfzeilen (Response). Den Body liest man mit einem zweiten await (res.json(), res.text() …).

Zeitleiste (echte Anfrage an die Beispiel-API)
Klicke einen der Knöpfe oben.

🧯Fehler in asynchronem Code

try/catch fängt nur mit await

1const kaputt = () => Promise.reject(new Error("Netz weg"));
2
3async function mitAwait() {
4 try { await kaputt(); }
5 catch (e) { console.log("gefangen:", e.message); }
6}
7async function ohneAwait() {
8 try { return kaputt(); } // kein await!
9 catch (e) { console.log("wird nie erreicht"); }
10}
11await mitAwait();
12await ohneAwait().catch((e) => console.log("erst außen:", e.message));
Konsole (echt ausgeführt)
wird ausgeführt …

Ohne await verlässt das abgelehnte Promise die Funktion, bevor try/catch es sehen kann. Im Zweifel: return await.

Unbehandelte Ablehnung

1Promise.reject(new Error("niemand hört zu"));
2setTimeout(() => console.log("Programm läuft weiter"), 10);
Konsole (echt ausgeführt)
wird ausgeführt …

Browser melden „Uncaught (in promise)“ in der Konsole, Node.js beendet den Prozess seit Version 15 standardmäßig.

Nacheinander vs. parallel

1const warte = (ms) => new Promise((r) => setTimeout(() => r(ms), ms));
2let t = Date.now();
3await warte(100); await warte(100);
4console.log("nacheinander ≥ 200 ms:", Date.now() - t >= 200);
5t = Date.now();
6await Promise.all([warte(100), warte(100)]);
7console.log("parallel < 200 ms:", Date.now() - t < 200);
Konsole (echt ausgeführt)
wird ausgeführt …

Unabhängige Aufgaben nicht einzeln awaiten, sondern gemeinsam starten und mit Promise.all abwarten.

✅ Checkliste
Jedes Promise braucht einen Abnehmer: await in try/catch oder ein .catch() am Ende der Kette. Kein async-Callback in forEach – das wartet nicht; stattdessen for…of mit await oder Promise.all.