Menu

JavaScript async/await: Asynchroner Code verständlich

Wie async/await in JavaScript wirklich funktioniert: async-Funktionen, Promises mit await abwarten, Fehler mit try/catch abfangen und Tasks parallel ausführen.

async/await ist im Grunde nur eine hübschere Promise-Syntax

async/await ist kein neues Nebenläufigkeitsmodell in JavaScript – es ist syntaktischer Zucker für Promises, mit dem sich asynchroner Code schreiben lässt, der wie sequentieller Code aussieht. Unter der Haube läuft dieselbe Mechanik, nur eben in einer deutlich angenehmeren Form.

Dieselbe Aufgabe einmal klassisch mit Promises und einmal mit async/await:

index.js
Output
Click Run to see the output here.

Beide Funktionen geben ein Promise zurück. Beide machen exakt dasselbe. Die async-Variante liest sich von oben nach unten, ganz ohne .then-Kette – und genau das ist der Reiz.

async kennzeichnet eine Funktion, die ein Promise zurückgibt

Setzt du async vor eine function, eine Arrow Function oder eine Methode, passieren zwei Dinge:

  1. Die Funktion gibt immer ein Promise zurück. Was du mit return zurückgibst, wird zum aufgelösten Wert.
  2. Du darfst darin await verwenden.
index.js
Output
Click Run to see the output here.

result ist nicht der String selbst, sondern ein Promise, das zu diesem String aufgelöst wird. Auch wenn greet kein einziges await enthält und keine asynchrone Arbeit leistet: Das Schlüsselwort async verpackt den Rückgabewert automatisch in ein Promise. Wirft die Funktion einen Fehler, wird das Promise abgelehnt.

await: warten, bis ein Promise fertig ist

Innerhalb einer async-Funktion hält await somePromise die Ausführung an, bis das Promise aufgelöst wurde – und liefert dir dann den aufgelösten Wert zurück. Wird das Promise abgelehnt, wirft await einen Fehler.

index.js
Output
Click Run to see the output here.

Achte auf die Ausgabereihenfolge: "Countdown gestartet" erscheint vor "2" – denn await pausiert lediglich die async-Funktion, nicht das restliche Programm. Die Event Loop läuft munter weiter; countdown macht erst dann weiter, wenn das jeweilige wait-Promise aufgelöst wird.

Übrigens lässt sich mit await alles Promise-Ähnliche abwarten. Selbst await 42 ist erlaubt – Werte, die keine Promises sind, werden automatisch in Promise.resolve(42) verpackt und sofort aufgelöst.

Fehlerbehandlung mit try/catch bei async/await

Bei klassischen Promises hängst du ein .catch() an die Kette. Mit async/await wird ein abgelehntes Promise dagegen zu einer ganz normalen Exception, die du wie gewohnt abfangen kannst:

index.js
Output
Click Run to see the output here.

Ein einziges try/catch deckt jedes await darin ab. Netzwerkfehler, JSON-Parsing-Probleme und deine eigenen throw-Statements landen alle im selben catch. Das ist ein echter Fortschritt gegenüber verschachtelten .then/.catch-Ketten.

Eine Sache musst du im Hinterkopf behalten: fetch wirft nur bei Netzwerkfehlern einen Fehler, nicht bei HTTP-Statuscodes wie 4xx oder 5xx. Du prüfst res.ok selbst und wirfst dann den Fehler – ein Muster, das dir in echtem Code ständig begegnet.

Kein await in Schleifen, wenn es nicht sein muss

Das ist der häufigste Stolperstein bei async/await. Ein sequenzielles await in einer Schleife bedeutet, dass jeder Durchlauf auf den vorherigen warten muss:

index.js
Output
Click Run to see the output here.

sequential braucht ca. 900 ms, parallel nur rund 300 ms. Faustregel: Wenn Tasks nicht voneinander abhängen, starte sie alle gleichzeitig und nutze dann await Promise.all. Nacheinander mit await arbeitest du nur, wenn der nächste Aufruf wirklich das Ergebnis des vorherigen braucht.

Für Listen ist Promise.all(items.map(async (x) => ...)) das Standardmuster. Eine klassische for...of-Schleife mit await im Inneren läuft sequenziell – manchmal willst du genau das (Rate-Limiting, feste Reihenfolge), meistens aber nicht.

async/await und klassische Promises mischen

Du musst dich nicht entscheiden: async-Funktionen geben Promises zurück, und await funktioniert mit jedem Promise. Beides lässt sich also problemlos kombinieren:

index.js
Output
Click Run to see the output here.

Beide Schreibweisen sind gleichwertig. Nimm await, wenn sich der Code von oben nach unten flüssiger liest; greif zu .then, wenn es nur um einen schnellen Einzeiler geht oder du dich außerhalb einer async-Funktion bewegst.

Top-Level await in ES-Modulen

Früher musstest du await zwingend in eine async-Funktion packen, weil es auf oberster Ebene eines Skripts nicht erlaubt war. Das hat sich geändert: In einem ES-Modul (also einer .mjs-Datei oder einem <script type="module">) kannst du await mittlerweile direkt auf der obersten Ebene einsetzen:

// in einem ES-Modul
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await res.json();
console.log(user.name);

Top-Level await verzögert das Fertigstellen des Moduls, bis das erwartete Promise aufgelöst ist — und jeder, der das Modul importiert, wartet mit. Praktisch für das Laden von Konfigurationen oder dynamische Imports, aber mit Bedacht einsetzen: Ein langsames Top-Level await blockiert alle Importeure.

In CommonJS-Dateien oder klassischen Inline-Skripten führt das nach wie vor zu einem SyntaxError. Der übliche Workaround ist eine sofort ausgeführte async-Funktion (IIFE):

index.js
Output
Click Run to see the output here.

Typische Stolperfallen bei async/await

Ein kurzer Rundgang durch die Fehler, die fast jedem mal passieren:

  • async vergessen. await in einer normalen Funktion ist ein Syntaxfehler. Die Lösung: entweder async davorsetzen — oder die async-Hilfsfunktion mit .then aufrufen.
  • Das Ergebnis nicht awaiten. const data = getJSON(url); liefert dir ein Promise zurück, nicht die eigentlichen Daten. Wenn du damit weiterarbeitest, als wäre es schon der Wert, taucht in der Ausgabe nur [object Promise] auf.
  • Nicht behandelte Rejections. Eine async-Funktion, die du einfach abschickst und vergisst (doWork();), schluckt Fehler still und heimlich — es sei denn, du hängst ein .catch dran oder packst den await in ein try/catch.
  • forEach mit async-Callbacks. array.forEach(async (x) => await something(x)) wartet auf gar nichts — forEach ignoriert die zurückgegebenen Promises schlicht. Nimm stattdessen for...of mit await oder Promise.all(array.map(...)).
index.js
Output
Click Run to see the output here.

Führe das aus — "beendet?" erscheint vor jedem "fertig", weil broken zurückkehrt, ohne zu warten. fixed wartet dagegen auf alles und gibt "beendet!" ganz am Ende aus.

Wann du zu async/await greifen solltest

Nutze async/await standardmäßig, sobald dein Code mehrere asynchrone Schritte nacheinander abarbeitet oder du eine Fehlerbehandlung im Stil von try/catch brauchst. Rohe Promises bleiben sinnvoll für triviale Einzeiler, für Bibliothekscode, der ein Promise zurückgibt, ohne selbst warten zu müssen, oder wenn du tatsächlich Kombinatoren wie Promise.race oder ein .finally() in einer Kette brauchst.

Richtig eingesetzt liest sich async/await wie ein Kochrezept: erst das, dann das, dann das. Die Event Loop macht weiterhin ihren Job — du musst nur nicht mehr in Callbacks denken.

Weiter geht's: die fetch-API

In den Beispielen hier stand fetch meistens stellvertretend für „irgendeine asynchrone Sache". Ein genauerer Blick lohnt sich aber — wie Requests und Responses funktionieren, wie du mit JSON umgehst, Header setzt und warum fetch bei HTTP-Fehlern kein Reject auslöst. Darum geht es auf der nächsten Seite.

Häufig gestellte Fragen

Was macht async/await in JavaScript eigentlich?

async/await ist eine Syntax für den Umgang mit Promises, mit der sich asynchroner Code lesen lässt wie synchroner. async markiert eine Funktion so, dass sie immer ein Promise zurückgibt, und await hält die Ausführung innerhalb dieser Funktion an, bis das Promise aufgelöst ist — danach bekommst du direkt den Wert. Unter der Haube bleibt es bei Promises, nur deutlich lesbarer.

Darf ich await auch außerhalb einer async-Funktion nutzen?

Auf der obersten Ebene eines ES-Moduls ja — das nennt sich Top-Level await. In normalen Funktionen oder CommonJS-Skripten nicht: Dort ist await außerhalb einer async-Funktion ein Syntaxfehler. Die Lösung ist meistens, den Code in eine async-Funktion zu packen und aufzurufen — oder die Datei auf ein ES-Modul umzustellen.

Wie behandelt man Fehler bei async/await?

Pack die await-Aufrufe in einen try/catch-Block. Jedes abgelehnte Promise, auf das du wartest, wird zu einer Exception, die du im catch abfangen kannst. Bei Hintergrund-Tasks, die du nicht awaitest, hängst du ein .catch() an das zurückgegebene Promise, damit aus Rejections keine Unhandled Promise Rejections werden.

Blockiert await das ganze Programm?

Nein. await pausiert nur die aktuelle async-Funktion. Die Event Loop läuft ganz normal weiter — Timer feuern, andere asynchrone Tasks kommen voran, das UI bleibt responsiv. Der Aufrufer bekommt sofort ein pending Promise zurück und macht seinerseits direkt weiter.

Lerne mit Coddy zu programmieren

LOS GEHT'S