Zum Inhalt springen

3.2 Asynchrones JavaScript: async/await und die typischen Fallen

Zu Zen-Modus wechseln

Nach dieser Einheit kannst du:

  • asynchrone Abläufe mit async/await schreiben, mit try/catch/finally absichern und in TypeScript mit Promise<T> typisieren.
  • die Ausgabereihenfolge von Code mit await vorhersagen und Callbacks, Promise-Kette und async/await vergleichen.
  • ein vergessenes await und ein Floating Promise an ihren Symptomen erkennen und mit await, .catch(...) oder void sowie der Biome-Regel noFloatingPromises beheben.

Du klickst in einer App auf „Speichern“, und sofort erscheint „gespeichert“. Eine Sekunde später scheitert die Verbindung zum Server. In der Konsole steht eine rote Zeile, die niemand liest, und deine Eingaben sind weg. Kein Absturz, keine Meldung auf dem Bildschirm, nur ein fehlendes Wort: await.

flowchart LR
    accTitle: Ein Promise ohne await scheitert unbemerkt
    btn["Klick auf Speichern"] --> save["saveReport();<br/>ohne await"]
    save --> shown["Anzeige: gespeichert"]
    save -.-> fail["später: Server<br/>nicht erreichbar"]
    fail --> nobody["niemand fängt<br/>den Fehler"]

Abb. 3.8: Das Programm meldet Erfolg, bevor das Speichern überhaupt fertig ist (eigene Darstellung).

In Einheit 3.1 hast du das Modell gebaut: Event Loop, zwei Warteschlangen und Promises. Diese Einheit zeigt zuerst async/await, die Schreibweise, mit der asynchroner Code wie gewöhnlicher Code aussieht. Gerade weil er so harmlos aussieht, übersieht man leicht ein fehlendes await. Deshalb folgen gleich danach die typischen Fallen.

  • async-Funktion: eine Funktion, die mit async markiert ist und immer ein Promise liefert.
  • await: wartet in einer async-Funktion auf ein Promise und liefert dessen Wert.
  • Floating Promise: das Promise einer aufgerufenen async-Funktion, das niemand erwartet und dessen Fehler niemand fängt.
  • Unhandled Rejection: ein abgelehntes Promise ohne Fehlerbehandlung. Node.js beendet dann das Programm.
  • Nursery-Regel: eine Linter-Regel in Erprobung, die du in Biome ausdrücklich einschalten musst.

Promise-Ketten sind Callbacks weit überlegen, aber immer noch eine eigene Denkform. Die Schlüsselwörter async und await machen daraus Code, der wie gewöhnlicher, zeilenweiser Code aussieht:

async function printInvoiceTotal(userId: string): Promise<void> {
try {
const user = await loadUser(userId);
const orders = await loadOrders(user);
const firstOrder = orders[0]; // Order | undefined (noUncheckedIndexedAccess)
if (!firstOrder) throw new Error(`user ${userId} has no orders`);
const invoice = await loadInvoice(firstOrder);
console.log(invoice.total);
} catch (error) {
console.error("failed:", error);
} finally {
console.log("done either way");
}
}

Beispielprojekt: 03_async_javascript_examples/ex06_async_await

Die Zeilen mit firstOrder kennst du aus Kapitel 1: Unter noUncheckedIndexedAccess hat orders[0] den Typ Order | undefined, weil die Liste leer sein könnte. Der Fehler, der im try geworfen wird, landet im selben catch wie ein gescheitertes Promise.

  • await darf nur in Funktionen stehen, die mit async markiert sind, und auf oberster Ebene eines Moduls.
  • await promise pausiert diese Funktion, bis das Promise erfüllt ist, und liefert dessen Wert. Pausiert wird nur die Funktion, nicht das Programm; der Event Loop arbeitet währenddessen anderes ab.
  • Eine async-Funktion liefert immer ein Promise, auch wenn sie scheinbar einen normalen Wert zurückgibt (return invoice wird zu Promise<Invoice>).
  • Ein rejected Promise wird bei await zu einer geworfenen Exception. Die Fehlerbehandlung ist damit das vertraute try/catch/finally, mit denselben Strategiefragen wie bei jeder Exception: fangen, wo eine sinnvolle Reaktion möglich ist, sonst weiterreichen.
Zeitleiste mit zwei Spuren. Oben deine async-Funktion: Sie läuft bis zum await, pausiert, bis das Promise erfüllt ist, und läuft dann weiter. Unten der Rest des Programms: Während der Pause arbeitet der Event Loop weiter, etwa einen Klick und einen Timer ab
Abb. 3.9: await pausiert nur die eigene Funktion, nicht das Programm (eigene Darstellung).

Genauer erklärt: Im Lokal heißt das, der Kellner merkt sich bei Tisch 1 “weiter, sobald das Schnitzel da ist” und geht inzwischen zu Tisch 2. Kommt während eines await ein Klick oder läuft ein Timer ab, wird dessen Callback sofort bearbeitet. Die Fortsetzung nach dem await kommt als Microtask dran, sobald das Promise erfüllt ist.

async/await ist reine Schreibweise über Promises, kein neues Ausführungsmodell: Hinter jedem await steckt ein then. Derselbe Ablauf “Benutzer, Bestellungen, Rechnung” sieht in den drei Formen aus Einheit 3.1 und dieser Einheit so aus:

CallbacksPromise-Ketteasync/await
FormTreppe, jede Stufe eingerücktflache Kette mit thenZeile für Zeile
Fehlerbehandlungauf jeder Stufe einzelnein catch am Endetry/catch/finally
Zwischenwertenur in der jeweiligen Stufe sichtbarnur im jeweiligen thennormale Variablen
Heute eingesetzt fürEreignisse wie Klicks und Timerkurze Einzeilerden Normalfall

Wer den Event Loop verstanden hat, kann alle drei Formen lesen. Im eigenen Code ist async/await der Standard, then-Ketten bleiben für kleine Einzeiler.

Wo begegnet dir das? In Next.js ab Kapitel 9: Eine Seite, die Daten vom Server braucht, ist dort eine async-Funktion, die ihre Daten mit await lädt, bevor sie die Seite erzeugt. Auch jedes Skript in Node.js, das Dateien liest oder Daten abruft, schreibst du ab jetzt so.

Das folgende Video (freiwillig, auf Englisch) zeigt die Abarbeitung von Promises Schritt für Schritt animiert und ergänzt diesen Abschnitt und die Warteschlangen aus Einheit 3.1 um eine visuelle Darstellung von Microtasks, Task-Queue und async/await: JavaScript Visualized - Promise Execution von Lydia Hallie.

Abspielen
Auflösung

Node.js gibt Promise { 42 } aus: Eine async-Funktion liefert immer ein Promise, auch bei return 42. An die Zahl kommst du mit console.log(await getAnswer()); in einer async-Funktion bzw. auf oberster Ebene eines Moduls, oder mit getAnswer().then((n) => console.log(n));.

Drei Fehler verursachen den Großteil aller Async-Bugs. Alle drei sind mit TypeScript, einem Linter und etwas Aufmerksamkeit vermeidbar. Die ersten beiden zeigt dieser Abschnitt, der dritte folgt in Einheit 3.3.

async function loadCount(): Promise<number> { /* ... */ }
const count = loadCount(); // forgot await
console.log(count + 1); // "[object Promise]1" nonsense

Beispielprojekt: 03_async_javascript_examples/ex07_forgotten_await

Ohne await landet das Promise selbst in der Variablen, nicht sein Wert. TypeScript macht daraus hier einen Compile-Fehler (Operator '+' cannot be applied to types 'Promise<number>' and '1'), einer der spürbarsten Vorteile der Typisierung in diesem Kapitel. Wie bei jedem Typfehler gilt aber: tsx startet das Programm trotzdem, erst npm run typecheck meldet ihn.

Verwandt ist das Floating Promise: eine aufgerufene async-Funktion, deren Ergebnis niemand beachtet. Scheitert sie, fängt niemand den Fehler. Was dann passiert, hängt von der Umgebung ab:

  • Node.js (ab Version 15) beendet das Programm mit einer Unhandled Rejection, aber erst in dem Moment, in dem der Fehler auftritt. Das ist womöglich lange nach dem Aufruf und nachdem das Programm schon “fertig” gemeldet hat. Die Fehlermeldung zeigt nicht, wo das await vergessen wurde.
  • Im Browser erscheint nur eine Meldung in der Konsole der DevTools. Die Seite läuft weiter, als wäre nichts gewesen.
  • In Frameworks und Servern, die einen globalen Handler für unbehandelte Fehler installieren, verschwindet der Fehler oft ganz.

In allen drei Fällen ist die eigentliche Aufgabe nicht erledigt worden, und niemand hat es an der richtigen Stelle bemerkt.

Oben das Floating Promise: saveReport(); ohne await, das Programm meldet fertig, später ist das Promise rejected und niemand fängt den Fehler; Node.js stürzt ab, der Browser schreibt nur in die Konsole. Unten drei bewusste Entscheidungen: await saveReport(); erwarten im try, saveReport().catch(logError); mit Fehlerpfad, void saveReport(); bewusst ignoriert
Abb. 3.10: Ein Floating Promise scheitert unbemerkt; jede der drei unteren Formen ist eine bewusste Entscheidung (eigene Darstellung).

Gegen dieses Fehlerbild hilft ein Linter, und zwar Biome, das du aus Einheit 1.1 kennst (Abschnitt „Biome: prüfen und formatieren mit einem Werkzeug”). Die passende Regel heißt noFloatingPromises. Sie ist allerdings noch eine Nursery-Regel, also eine Regel in Erprobung, und gehört nicht zum empfohlenen Regelsatz. Mit der biome.json, die biome init anlegt, meldet Biome ein Floating Promise deshalb gar nicht. Du musst die Regel ausdrücklich einschalten:

{
"linter": {
"enabled": true,
"rules": {
"preset": "recommended",
"nursery": {
"noFloatingPromises": "error"
}
}
}
}

Dann meldet biome check jeden Aufruf einer async-Funktion, dessen Promise niemand behandelt: in einer anderen async-Funktion, als main(); auf oberster Ebene, bei Funktionen aus einem anderen Modul und auch in reinen .js-Dateien. Als behandelt gilt ein Promise, das du mit await erwartest, das mit .catch(...) einen Fehlerpfad bekommt oder das du mit void ausdrücklich als „bewusst ignoriert” markierst.

Genauer erklärt: Aus saveReport(); wird je nach Absicht await saveReport();, saveReport().catch(logError); oder void saveReport();, und die Entscheidung steht im Code, statt verloren zu gehen. Biome bietet als Korrektur „await ergänzen” an, aber nur als unsafe fix, den du selbst bestätigst, weil ein zusätzliches await den Ablauf verändert.

Wo begegnet dir das? Ein Formular zeigt nach dem Klick auf “Speichern” sofort “gespeichert”, weil der Code das Promise des Speicherns nie erwartet hat. Scheitert die Verbindung zum Server, steht nur eine rote Zeile in der Konsole, die niemand liest, und die Eingaben sind weg. Genau das zeigt Abb. 3.8 am Anfang dieser Einheit.

Auflösung

(a) await saveReport();, am besten in einem try/catch, damit ein Fehler statt „gespeichert“ gemeldet wird. (b) sendStatistics().catch(logError);: Der Ablauf wartet nicht, der Fehler hat trotzdem einen Pfad. (c) void prefetchImages();: bewusst ignoriert. Das ist nur sicher, weil die Funktion ihre Fehler selbst fängt; void allein fängt keinen Fehler ab. Biome akzeptiert alle drei Formen.

  1. “await hält das Programm an”: Pausiert wird nur die eigene async-Funktion. Der Event Loop arbeitet währenddessen Klicks, Timer und andere Callbacks ab.
  2. Den Wert einer async-Funktion direkt verwenden: console.log(getAnswer()) zeigt Promise { 42 }, nicht 42. An den Wert kommst du mit await oder then.
  3. Vergessenes await: [object Promise] in der Ausgabe oder eine Dauer von fast 0 ms bei einer eigentlich langsamen Operation.
  4. try/catch ohne await: Der try-Block ist schon verlassen, wenn das Promise später scheitert; das catch läuft nie, und Node.js beendet das Programm mit einer Unhandled Rejection.
  5. .catch() ohne Rückgabewert: Die Kette läuft mit undefined weiter. Im catch einen Ersatzwert liefern oder den Fehler weiterwerfen.
  6. Biome meldet nichts, also gibt es keine Floating Promises: noFloatingPromises ist nicht im empfohlenen Regelsatz. Ohne den nursery-Eintrag in der biome.json bleibt jedes vergessene Promise still.
  7. void als Fehlerbehandlung missverstanden: void markiert ein Promise nur als bewusst ignoriert. Scheitert es, beendet Node.js das Programm trotzdem mit einer Unhandled Rejection. void passt nur, wenn die Funktion ihre Fehler selbst fängt.
  8. Programm läuft, also passt das await: tsx und der Vite-Dev-Server prüfen keine Typen. Den Compile-Fehler beim vergessenen await zeigt erst npm run typecheck.

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:

  • asynchrone Abläufe mit async/await schreiben, mit try/catch/finally absichern und in TypeScript mit Promise<T> typisieren.
  • die Ausgabereihenfolge von Code mit await vorhersagen und Callbacks, Promise-Kette und async/await vergleichen.
  • ein vergessenes await und ein Floating Promise an ihren Symptomen erkennen und mit await, .catch(...) oder void sowie der Biome-Regel noFloatingPromises beheben.
  • Aufgabe 03-1a - Reihenfolge-Rätsel, Stufe Wesentlich_vollständig (Rätsel mit async/await)
  • Aufgabe 03-2a - Promise-Werkstatt, Stufen Wesentlich_überwiegend und Wesentlich_vollständig
  • Regeln für async und await (Rückgabewert, geworfene Exception) → MDN: async function und await
  • noUncheckedIndexedAccess (Typ T | undefined bei Array-Zugriff) → TSConfig-Referenz: noUncheckedIndexedAccess; Compiler-Meldung beim vergessenen await → eigener Test mit TypeScript 7.0.2 (Oktober 2026)
  • Node.js beendet das Programm bei einer Unhandled Rejection (ab Version 15) → Node.js-Blog: Node v15.0.0 und Node.js-Dokumentation: Event ‘unhandledRejection’; Verhalten mit Node.js 24.11.1 selbst getestet, auch dass void eine Ablehnung nicht abfängt
  • Browser meldet unbehandelte Ablehnungen nur in der Konsole → MDN: Window: unhandledrejection event
  • Biome-Regel noFloatingPromises (Nursery, Einschalten in biome.json, akzeptierte Schreibweisen, unsafe fix) → Biome: noFloatingPromises; Verhalten mit Biome 2.5.15 und Node.js 24.11.1 selbst getestet
  • Biome allgemein, biome init, biome check → Biome: Getting Started
  • Ausgabe Promise { 42 } → eigener Test mit Node.js 24.11.1 (Oktober 2026)
  • Video zur Abarbeitung von Promises → „JavaScript Visualized - Promise Execution”, Lydia Hallie, eingebettet, nicht kopiert
  • Abb. 3.8 (Speichern ohne await, Mermaid), Abb. 3.9 (await pausiert die Funktion), Abb. 3.10 (Floating Promise) → eigene Darstellung
  • Oliver Maurhart (MAO)
  • Michael Gabriel (GAM)

CC-BY-SA

https://qiz.at/rules