Zum Inhalt springen

3.3 Asynchrones JavaScript: sequentiell oder parallel

Zu Zen-Modus wechseln

Nach dieser Einheit kannst du:

  • entscheiden, welche Schritte eines Ablaufs voneinander abhängen, und unabhängige Schritte mit Promise.all gleichzeitig starten.
  • await in Schleifen bewusst einsetzen oder durch Promise.all über map ersetzen.
  • einen vollständigen asynchronen Ablauf mit Promise<T>, Promise.all und einem einzigen catch lesen und seine Laufzeit begründen.

Eine App lädt beim Start dein Profil, deine Benachrichtigungen und die Neuigkeiten. Jede Anfrage dauert eine Sekunde. Wartest du drei Sekunden auf die Startseite oder nur eine? Beides ist möglich, und der Unterschied steckt in einer einzigen Codezeile.

flowchart LR
    accTitle: Drei unabhängige Anfragen beim App-Start
    start["App-Start"] --> profile["Profil laden"]
    start --> notes["Benachrichtigungen laden"]
    start --> news["Neuigkeiten laden"]
    profile --> ready["Startseite fertig"]
    notes --> ready
    news --> ready

Abb. 3.11: Drei Anfragen, die nicht aufeinander angewiesen sind: Müssen sie trotzdem aufeinander warten? (eigene Darstellung).

In Einheit 3.2 hast du zwei Fallen kennengelernt, die zu Abstürzen oder verschwundenen Fehlern führen. Die dritte Falle stürzt nicht ab, sie verschenkt Zeit. Am Ende dieser Einheit setzt ein vollständiges Beispiel alles aus Kapitel 3 zusammen.

  • sequentiell: Schritte laufen nacheinander, jeder wartet, bis der vorige fertig ist.
  • parallel (hier genauer: nebenläufig): Schritte laufen gleichzeitig, gewartet wird auf alle gemeinsam.
  • abhängig: Ein Schritt braucht das Ergebnis eines anderen, etwa die Bestellungen die Benutzer-ID.
  • Promise.all: nimmt mehrere Promises und liefert ein Promise, das erfüllt ist, sobald alle erfüllt sind, und abgelehnt wird, sobald eines scheitert.

Die dritte Falle aus Einheit 3.2 ist kein Absturz, sondern verschenkte Zeit. Jedes await wartet, bevor die nächste Zeile startet, auch wenn die nächste Zeile gar nicht auf das Ergebnis angewiesen ist.

// sequential: second request starts only after the first finished
const user = await loadUser(id);
const settings = await loadSettings(id); // does NOT depend on user
// parallel: both start immediately, wait for both
const [user2, settings2] = await Promise.all([loadUser(id), loadSettings(id)]);

Beispielprojekt: 03_async_javascript_examples/ex08_sequential_vs_parallel

Bei unabhängigen Operationen verschenkt das Nacheinander Zeit: Zwei Anfragen zu je einer Sekunde dauern nacheinander zwei Sekunden, parallel eine (im Beispielprojekt gemessen: rund 2000 ms gegen rund 1000 ms). Die beiden Aufrufe im Array starten beide Anfragen sofort, und Promise.all wartet, bis alle erfüllt sind. Die Koordinationswerkzeuge dafür (Promise.all und Verwandte) behandelt das nächste Kapitel im Detail, hier genügt das Muster.

Zeitleiste von 0 bis 2 Sekunden. Oben nacheinander mit zwei await: loadUser dauert eine Sekunde, erst danach startet loadSettings, zusammen etwa 2 Sekunden. Unten parallel mit Promise.all: loadUser und loadSettings laufen gleichzeitig, zusammen etwa 1 Sekunde. Daneben der Hinweis: nur für unabhängige Schritte
Abb. 3.12: Zwei unabhängige Anfragen nacheinander und parallel (eigene Darstellung).
// one after another: n * latency
for (const id of ids) {
results.push(await loadUser(id));
}
// all at once: roughly 1 * latency
const results = await Promise.all(ids.map((id) => loadUser(id)));

Beispielprojekt: 03_async_javascript_examples/ex09_await_in_loop

Mit vier Benutzern zu je einer halben Sekunde misst das Beispielprojekt rund 2000 ms für die Schleife und rund 500 ms für Promise.all. Die Schleifenform ist trotzdem nicht falsch: Manchmal ist die Reihenfolge wichtig, oder der Server soll nicht mit vielen gleichzeitigen Anfragen belastet werden. Sie muss aber eine Entscheidung sein, kein Versehen.

Wo begegnet dir das? Wenn eine App beim Start dein Profil, deine Benachrichtigungen und die Neuigkeiten lädt, hängen diese drei Anfragen nicht voneinander ab. Gemeinsam gestartet bestimmt die langsamste die Wartezeit, nacheinander addieren sich alle drei.

Auflösung

Benutzer und Bestellungen hängen voneinander ab: erst await loadUser(...), dann mit dessen ID await loadOrders(...). Das Wetter braucht keines der beiden Ergebnisse und kann gleichzeitig mit diesem Paar starten, etwa mit Promise.all([userAndOrders(), loadWeather()]), wobei userAndOrders eine async-Funktion mit den beiden await ist.

Ein Ausschnitt aus einem Render-Dashboard: Der Status mehrerer Render-Jobs wird simuliert abgefragt, mit Wartezeit, Fehlerfall und Messung. Hier kommt alles aus Kapitel 3 zusammen: Promise<T>, async/await, Promise.all und ein einziges catch.

interface RenderJob {
id: string;
state: "queued" | "running" | "done" | "failed";
}
function sleep(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function fetchJobState(id: string): Promise<RenderJob> {
await sleep(300 + Math.random() * 200); // simulated network delay
if (id === "job-13") {
throw new Error(`job ${id}: render node not reachable`);
}
return { id, state: "running" };
}
async function report(ids: string[]): Promise<void> {
const start = performance.now();
try {
const jobs = await Promise.all(ids.map((id) => fetchJobState(id)));
for (const job of jobs) {
console.log(`${job.id}: ${job.state}`);
}
} catch (error) {
console.error("report incomplete:", (error as Error).message);
}
console.log(`took ${Math.round(performance.now() - start)} ms`);
}
await report(["job-11", "job-12", "job-14"]);

Beispielprojekt: 03_async_javascript_examples/ex10_render_dashboard

Miss selbst: Mit drei Jobs dauert der parallele Bericht rund eine halbe Sekunde (gemessen: 453 ms), weil Promise.all nur auf den langsamsten Job wartet. Nacheinander wären es drei Wartezeiten von je 300 bis 500 ms, also 0,9 bis 1,5 Sekunden.

Die Abfragen für job-11, job-12 und job-14 starten gleichzeitig. await Promise.all wartet auf die langsamste, etwa 0,5 Sekunden statt 1,5 Sekunden. Steht job-13 in der Liste, ist dessen Abfrage rejected, und ein einziges catch meldet report incomplete. Der Ausfall verschwindet nicht, er wird an einer Stelle gemeldet
Abb. 3.13: Promise.all wartet auf den langsamsten Job, ein Ausfall landet im einen catch (eigene Darstellung).

Wo begegnet dir das? Statusseiten funktionieren nach diesem Muster, etwa die Übersicht einer Build-Pipeline oder einer Paketverfolgung mit mehreren Sendungen: Viele unabhängige Abfragen starten gleichzeitig, und ein Ausfall muss sichtbar gemeldet werden, statt still zu verschwinden.

Auflösung

Ausgegeben werden nur report incomplete: job job-13: render node not reachable und die Dauer. job-11 und job-14 erscheinen nicht: Promise.all wird abgelehnt, sobald ein einziges Promise scheitert, und die for-Schleife läuft nie. Die Dauer liegt dann zwischen 300 und 500 ms, weil Promise.all nicht auf die übrigen Jobs wartet (in Node.js 24 gemessen: 359 bis 436 ms).

  1. Unnötig sequentiell: Unabhängige Anfragen nacheinander awaiten verschenkt Zeit.
  2. forEach mit async-Callback: forEach wartet nicht und verwirft die Promises samt Fehlern. Ersatz: for...of mit await oder Promise.all über map.
  3. Promise.all liefert bei einem Fehler gar nichts: Scheitert ein einziges Promise, wird Promise.all abgelehnt, und auch die erfolgreichen Ergebnisse kommen nicht an.
  4. Abhängige Schritte parallel gestartet: Braucht ein Schritt das Ergebnis eines anderen, muss er warten; Promise.all passt nur für unabhängige Schritte.

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.

Du kannst jetzt:

  • entscheiden, welche Schritte eines Ablaufs voneinander abhängen, und unabhängige Schritte mit Promise.all gleichzeitig starten.
  • await in Schleifen bewusst einsetzen oder durch Promise.all über map ersetzen.
  • einen vollständigen asynchronen Ablauf mit Promise<T>, Promise.all und einem einzigen catch lesen und seine Laufzeit begründen.
  • Aufgabe 03-2a - Promise-Werkstatt, Stufen PlusPlus und MandalorianPlus
  • Aufgabe 03-3a - Promise-Fehlerjagd
  • Promise.all für unabhängige Operationen, Ablehnung beim ersten Fehler → MDN: Promise.all()
  • for...of → MDN: for…of
  • Lehrplanbezug (Threading) → Lehrplan der Höheren Lehranstalt für Informationstechnologie, B.2 Medientechnik, 2.1 Softwareentwicklung, IV. Jahrgang (zitiert auf der Kursseite Lehrplan und Ablauf)
  • Messwerte (ex08, ex09, ex10) → eigene Messung mit Node.js 24.11.1 und tsx (Windows, Oktober 2026)
  • Abb. 3.11 (App-Start, Mermaid), Abb. 3.12 (sequentiell und parallel), Abb. 3.13 (Promise.all im Render-Dashboard) → eigene Darstellung
  • Oliver Maurhart (MAO)
  • Michael Gabriel (GAM)

CC-BY-SA

https://qiz.at/rules