4.1 Koordination: Kombinatoren, Timeout und Abbruch
Nach dieser Einheit kannst du:
- mehrere asynchrone Abläufe mit
Promise.all,Promise.allSettled,Promise.raceundPromise.anykoordinieren und die Wahl fachlich begründen. - einen Timeout mit
Promise.racebauen und eine Operation mitAbortControllertatsächlich abbrechen. - Laufzeiten mit
performance.now()messen und eine Entscheidung für oder gegen paralleles Starten mit Zahlen belegen.
Der Kellner aus Kapitel 3 bekommt einen Tisch für vier. Vier Gäste, vier Gerichte, und serviert wird erst, wenn alle Teller fertig sind; sonst isst einer allein, und die anderen schauen zu. Aber was, wenn ein Schnitzel verbrennt? Wartet der ganze Tisch? Und wie lange wartet ein Gast, bevor er aufsteht und geht?
flowchart LR
accTitle: Ein Tisch für vier: Wann wird serviert?
k1["Teller 1 fertig"] --> table{"Tisch für vier:<br/>wann servieren?"}
k2["Teller 2 fertig"] --> table
k3["Teller 3 verbrannt"] --> table
table --> all["alle oder keiner"]
table --> each["jeder Teller für sich"]
Abb. 4.1: Vier Gerichte, eine Entscheidung: Wird erst serviert, wenn alle fertig sind, oder jeder Teller, sobald er da ist? (eigene Darstellung).
Mit async/await aus Kapitel 3 schreibst du einen asynchronen Ablauf. Reale Anwendungen haben selten nur einen: Drei Datenquellen sollen gleichzeitig geladen werden, eine Anfrage soll nach zwei Sekunden aufgeben. Diese Einheit zeigt, wie du mehrere Abläufe koordinierst: mit den Kombinatoren der Promise-Familie, mit Timeouts und mit echtem Abbruch. Einheit 4.2 folgt mit Race Conditions und Workern.
Begriffe vorab
Abschnitt betitelt „Begriffe vorab“- Kombinator: eine Funktion, die mehrere Promises zu einem einzigen Promise zusammenfasst, etwa
Promise.all. - Timeout: eine Zeitgrenze, nach der eine Operation als gescheitert gilt.
- Abbruch (abort): eine laufende Operation wird beendet, statt dass nur niemand mehr auf ihr Ergebnis wartet.
- Signal: das Objekt, über das ein
AbortControllereiner Operation mitteilt, dass sie abbrechen soll.
Die Promise-Kombinatoren
Abschnitt betitelt „Die Promise-Kombinatoren“Vier statische Methoden fassen mehrere Promises zu einem zusammen. Sie unterscheiden sich darin, wann das Gesamtergebnis feststeht und was bei Fehlern passiert.
| Kombinator | erfüllt, wenn … | scheitert, wenn … | typischer Einsatz |
|---|---|---|---|
Promise.all | alle erfüllt sind | eines scheitert | zusammengehörige Daten laden |
Promise.allSettled | alle fertig sind, egal wie | nie | Sammelaktionen mit Einzelbericht |
Promise.race | das erste fertig ist, egal wie | das erste scheitert | Timeouts |
Promise.any | das erste erfüllt ist | alle scheitern | schnellste von mehreren Quellen |
Je ein realistischer Fall:
// all: a dashboard needs user, jobs and quota; missing one means broken pageconst [user, jobs, quota] = await Promise.all([ loadUser(id), loadJobs(id), loadQuota(id),]);
// allSettled: send 50 newsletters; one bounce must not stop the restconst results = await Promise.allSettled(recipients.map((r) => sendMail(r)));const failed = results.filter((r) => r.status === "rejected").length;console.log(`${results.length - failed} sent, ${failed} failed`);
// any: three mirror servers, take whichever answers firstconst file = await Promise.any(mirrors.map((url) => download(url)));Beispielprojekt: 04_nebenlaeufigkeit_koordination_examples/ex01_combinators
allSettled liefert pro Promise ein Objekt { status: "fulfilled", value } oder { status: "rejected", reason }. Im Beispielprojekt prallen 7 der 50 Mails ab, die Ausgabe lautet 43 sent, 7 failed. any wartet auf das erste erfüllte Promise: Der schnellste Spiegelserver ist dort ausgefallen, also gewinnt der zweitschnellste. Scheitern alle, wird any mit einem AggregateError abgelehnt, der alle Einzelfehler in errors sammelt.
Die Entscheidung zwischen all und allSettled ist eine fachliche. Im Bild vom Lokal: Ein Tisch für vier wird gemeinsam serviert (all), beim Lieferdienst geht jede Bestellung für sich hinaus (allSettled).
flowchart LR
accTitle: all gegen allSettled bei einem Fehlschlag
p["drei Promises,<br/>eines scheitert"] --> all["Promise.all:<br/>abgelehnt, nur der Fehler"]
p --> settled["Promise.allSettled:<br/>2 × fulfilled, 1 × rejected"]
Abb. 4.2: Bei einem Fehlschlag verliert all auch die gelungenen Ergebnisse, allSettled berichtet jedes einzeln (eigene Darstellung).
Konkret heißt das: Ein Dashboard ohne Benutzerdaten ist kaputt, also all mit einem gemeinsamen catch. Ein Newsletter, der an 43 von 50 Adressen ankommt, ist trotzdem verschickt, also allSettled mit Auswertung pro Eintrag.
Wo begegnet dir das? Lädt eine Messenger-App mehrere Fotos hoch, zeigt sie danach “4 von 5 gesendet” und bietet für das fünfte einen neuen Versuch an. Dahinter steckt dasselbe Prinzip wie bei allSettled: Jeder Upload zählt für sich.
Auflösung
(a) Promise.all: Ohne einen Teil ist die Seite wertlos, ein gemeinsames catch zeigt die Fehlermeldung. (b) Promise.allSettled: Jedes Zeugnis zählt für sich, danach werden die fehlgeschlagenen gemeldet. (c) Promise.any: Die erste erfüllte Antwort gewinnt; ein ausgefallener Server stört nicht, solange einer antwortet. Promise.race wäre hier falsch, weil schon der erste Fehler das Rennen beenden würde.
Zeit und Abbruch
Abschnitt betitelt „Zeit und Abbruch“Timeout mit race
Abschnitt betitelt „Timeout mit race“Promise.race lässt Abläufe gegeneinander antreten; mit einem Timer als Gegner entsteht ein Timeout:
function timeout(milliseconds: number): Promise<never> { return new Promise((_, reject) => setTimeout(() => reject(new Error(`timed out after ${milliseconds} ms`)), milliseconds));}
const jobs = await Promise.race([loadJobs(id), timeout(2000)]);Beispielprojekt: 04_nebenlaeufigkeit_koordination_examples/ex02_timeout_race
Antwortet loadJobs innerhalb von zwei Sekunden, gewinnt es; sonst gewinnt der Timer, und die Ausführung landet im catch. Der Typ Promise<never> sagt präzise: Dieses Promise wird niemals erfüllt, es kann nur scheitern.
race hat einen Schönheitsfehler: Der Verlierer läuft im Hintergrund weiter. Im Lokal geht der Gast nach zehn Minuten, aber die Küche kocht das Schnitzel trotzdem fertig. Im Beispielprojekt zeigt sich das an der Meldung loadJobs finished, die auch dann noch erscheint, wenn der Timer gewonnen hat.
sequenceDiagram
accTitle: race beendet das Warten, nicht die Arbeit
participant code as dein Code
participant jobs as loadJobs
participant timer as timeout(2000)
code->>jobs: starten
code->>timer: starten
timer-->>code: nach 2 s: Fehler, race ist entschieden
jobs-->>jobs: arbeitet weiter
jobs-->>code: später: Ergebnis, das niemand mehr will
Abb. 4.3: Der Timer gewinnt das Rennen, aber loadJobs arbeitet trotzdem zu Ende (eigene Darstellung).
Wirklich abbrechen: AbortController
Abschnitt betitelt „Wirklich abbrechen: AbortController“Für echtes Abbrechen gibt es den AbortController, den unter anderem fetch (Kapitel 5) direkt unterstützt:
const controller = new AbortController();setTimeout(() => controller.abort(), 2000); // give up after 2 s
try { const response = await fetch(url, { signal: controller.signal });} catch (error) { // on abort: an AbortError lands here}Beispielprojekt: 04_nebenlaeufigkeit_koordination_examples/ex03_abort_controller
Der Controller besitzt ein signal, das du der Operation übergibst; abort() bricht alles ab, was an diesem Signal hängt. Im Beispielprojekt antwortet ein lokaler Server erst nach fünf Sekunden, nach zwei Sekunden meldet Node.js 24 AbortError: This operation was aborted, und die Verbindung wird geschlossen. Für den häufigsten Fall gibt es eine Kurzform: fetch(url, { signal: AbortSignal.timeout(2000) }) bricht nach zwei Sekunden selbst ab, der Fehler heißt dann TimeoutError.
Wo begegnet dir das? In einer Suchleiste, die schon beim Tippen Vorschläge lädt: Tippst du weiter, ist die vorige Anfrage überholt. Gute Apps brechen sie ab, statt auf eine Antwort zu warten, die niemand mehr anzeigt. Einheit 4.2 kommt darauf zurück.
Auflösung
Ja, loadJobs läuft weiter und verbraucht Netz und Server-Zeit; nur wartet niemand mehr auf das Ergebnis. Damit die Anfrage wirklich endet, muss loadJobs ein signal annehmen und an fetch weitergeben, und der Timer ruft controller.abort() auf. Dann bricht fetch die Verbindung ab, und es entsteht ein AbortError.
Messen statt glauben
Abschnitt betitelt „Messen statt glauben“Behauptungen über Parallelität sind billig; Zahlen überzeugen. Das Messwerkzeug ist performance.now():
async function measure(label: string, action: () => Promise<unknown>) { const start = performance.now(); await action(); console.log(`${label}: ${Math.round(performance.now() - start)} ms`);}
await measure("sequential", async () => { for (const id of ids) await fetchJobState(id);});await measure("parallel", () => Promise.all(ids.map((id) => fetchJobState(id))));Beispielprojekt: 04_nebenlaeufigkeit_koordination_examples/ex04_measure
Fünf simulierte Anfragen zu je rund 300 Millisekunden ergeben im Beispielprojekt 1567 gegen 327 Millisekunden.
xychart-beta
accTitle: Fünf Anfragen nacheinander und gemeinsam gestartet
title "Fünf Anfragen zu je rund 300 ms"
x-axis ["nacheinander", "gemeinsam (Promise.all)"]
y-axis "Dauer in ms" 0 --> 1800
bar [1567, 327]
Abb. 4.4: Messung im Beispielprojekt ex04_measure mit Node.js 24.11.1 (eigene Darstellung).
Solche Messungen gehören zu jeder Entscheidung, Abläufe gemeinsam zu starten: Sie belegen den Gewinn, und sie entlarven Fälle, in denen er ausbleibt. Promise.all ist immer so langsam wie sein langsamstes Promise; dominiert ein einzelner langsamer Teilnehmer, bringt das gemeinsame Starten wenig.
Wo begegnet dir das? Die Netzwerk-Ansicht der Browser-DevTools zeigt für jede Anfrage einer Webseite einen Balken auf einer Zeitachse. Dort siehst du sofort, welche Anfragen gleichzeitig laufen und welche aufeinander warten.
Stolperfallen
Abschnitt betitelt „Stolperfallen“Promise.allfür eine Sammelaktion: Ein einziger Fehlschlag verwirft auch alle gelungenen Ergebnisse. Ist jeder Teil für sich wertvoll,Promise.allSettlednehmen.racestattany:Promise.raceendet schon beim ersten Fehler. Wer die erste gültige Antwort will, brauchtPromise.any.- Timeout mit
racefür echten Abbruch gehalten: Der Verlierer arbeitet weiter. Für echten Abbruch einsignalmitAbortControlleroderAbortSignal.timeout(...)übergeben. - Parallelität behauptet, nicht gemessen: Ohne Vergleichswert ist ein Zeitgewinn nur eine Vermutung.
Zusatzmaterialien
Abschnitt betitelt „Zusatzmaterialien“- MDN: Promise.allSettled() - Rückgabeformat mit
status,valueundreason. - MDN: Promise.any() - erstes erfülltes Promise und
AggregateError. - MDN: AbortController - Signal,
abort()und Beispiele mitfetch. - javascript.info: Promise API - alle Kombinatoren im Vergleich, mit Aufgaben.
Zum Anhören
Abschnitt betitelt „Zum Anhören“Du kannst dir diese Einheit kostenlos mit der Vorlesefunktion deines Browsers oder Betriebssystems vorlesen lassen (etwa „Laut vorlesen” in Edge), ohne Konto und ohne Upload; die Codebeispiele liest du dabei am Bildschirm mit. Freiwillig kannst du mit Google NotebookLM (Google-Konto nötig) aus diesem öffentlichen Text eine gesprochene Zusammenfassung erzeugen, auch auf Deutsch.
Lernergebnisse
Abschnitt betitelt „Lernergebnisse“Du kannst jetzt:
- mehrere asynchrone Abläufe mit
Promise.all,Promise.allSettled,Promise.raceundPromise.anykoordinieren und die Wahl fachlich begründen. - einen Timeout mit
Promise.racebauen und eine Operation mitAbortControllertatsächlich abbrechen. - Laufzeiten mit
performance.now()messen und eine Entscheidung für oder gegen paralleles Starten mit Zahlen belegen.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 04-1a - Parallel oder nacheinander?, Stufen Wesentlich_überwiegend und Wesentlich_vollständig
Quellen
Abschnitt betitelt „Quellen“- Kombinatoren
all,allSettled,race,any→ MDN: Promise, Promise.allSettled(), Promise.any(), Promise.race() AbortController,AbortSignal.timeout()→ MDN: AbortController, AbortSignal.timeout()AggregateErrorbeiany, Verlierer vonraceläuft weiter,AbortErrorundTimeoutErrorbeifetch→ eigener Test mit Node.js 24.11.1 (Oktober 2026)performance.now()→ MDN: Performance.now()- Messwerte (
ex04_measure: 1567 ms gegen 327 ms) → eigene Messung mit Node.js 24.11.1 und tsx (Windows, Oktober 2026) - Abb. 4.1 (Tisch für vier), Abb. 4.2 (
allgegenallSettled), Abb. 4.3 (raceund der Verlierer), Abb. 4.4 (Messung) → eigene Darstellung, alle als Mermaid-Diagramm
Autoren
Abschnitt betitelt „Autoren“- Oliver Maurhart (MAO)
- Michael Gabriel (GAM)
CC-BY-SA