JavaScript async/await und Promises verständlich erklärt

Asynchrones Programmieren gehört zu den Themen, an denen viele Einsteiger in JavaScript zuerst hängenbleiben. Der Grund: JavaScript erledigt vieles nicht sofort, sondern wartet auf Daten aus dem Netzwerk, auf eine Datei oder auf einen Timer – und führt den restlichen Code in der Zwischenzeit weiter aus. Wer das nicht versteht, wundert sich, warum ein Wert „undefined" ist, obwohl er doch eindeutig gesetzt wurde. Dieser Ratgeber erklärt Schritt für Schritt, wie Promises funktionieren, was async und await daraus machen und wie Sie typische Fehler vermeiden.

Warum JavaScript überhaupt asynchron arbeitet

JavaScript läuft im Browser in einem einzigen Haupt-Thread. Würde der Code bei jeder Netzwerkanfrage einfach stehenbleiben und warten, könnte die Seite weder Klicks verarbeiten noch scrollen – sie würde einfrieren. Stattdessen reicht JavaScript langsame Aufgaben an die Umgebung weiter (Browser oder Node.js) und macht in der Zwischenzeit weiter. Sobald das Ergebnis da ist, wird der zugehörige Code nachträglich ausgeführt. Dieses Prinzip nennt man nicht-blockierend. Früher wurde das mit verschachtelten Callback-Funktionen gelöst, was schnell unübersichtlich wurde – die berüchtigte „Callback-Hölle". Promises und später async/await sind die Antwort darauf.

Was ein Promise ist: die drei Zustände

Ein Promise ist ein Objekt, das für einen Wert steht, der erst später verfügbar ist – eine Art Quittung für ein zukünftiges Ergebnis. Ein Promise befindet sich immer in genau einem von drei Zuständen:

Wichtig: Ein Promise wechselt seinen Zustand nur ein einziges Mal. Von pending geht es entweder nach fulfilled oder nach rejected – danach ist es „settled" (abgeschlossen) und ändert sich nicht mehr. Auf das Ergebnis reagiert man klassisch mit .then() für den Erfolgsfall und .catch() für den Fehlerfall:

fetch("/api/daten")
  .then(antwort => antwort.json())
  .then(daten => console.log(daten))
  .catch(fehler => console.error(fehler));

async und await: lesbarer Code statt Verschachtelung

Mit async/await lässt sich derselbe Ablauf schreiben, als wäre er synchron – von oben nach unten lesbar. Zwei Regeln sollten Sie sich merken:

  1. Eine mit async markierte Funktion gibt immer ein Promise zurück. Auch wenn Sie nur eine Zahl zurückgeben, wird sie automatisch in ein erfülltes Promise verpackt.
  2. await pausiert nur die aktuelle async-Funktion, bis das Promise abgeschlossen ist. Der restliche Code außerhalb läuft weiter – der Haupt-Thread blockiert nicht.

Das obige Beispiel sieht mit async/await so aus:

async function ladeDaten() {
  const antwort = await fetch("/api/daten");
  const daten = await antwort.json();
  console.log(daten);
}

Der Code ist kürzer, ohne Einrückungskaskade und deutlich leichter zu lesen. Unter der Haube passiert dasselbe wie mit .then() – async/await ist nur eine angenehmere Schreibweise für Promises.

Fehler richtig behandeln mit try/catch

Bei Promises fängt man Fehler mit .catch(). Bei async/await nutzt man stattdessen das vertraute try/catch aus der synchronen Welt. Das hält die Fehlerbehandlung zusammen mit dem Code, der den Fehler verursachen kann, und vermeidet verstreute Catch-Aufrufe:

async function ladeDaten() {
  try {
    const antwort = await fetch("/api/daten");
    if (!antwort.ok) throw new Error("Serverfehler " + antwort.status);
    return await antwort.json();
  } catch (fehler) {
    console.error("Laden fehlgeschlagen:", fehler);
    return null;
  }
}

Ein häufig übersehener Punkt: fetch wirft bei einem HTTP-Fehlerstatus wie 404 oder 500 keinen Fehler – das Promise wird trotzdem erfüllt. Nur Netzwerkprobleme führen zu einer Ablehnung. Deshalb sollte man den Status mit antwort.ok selbst prüfen und bei Bedarf einen Fehler werfen, wie im Beispiel gezeigt.

Mehrere Aufgaben parallel ausführen

Sobald mehrere unabhängige Aufgaben anstehen, lohnt sich Parallelität. JavaScript bietet dafür mehrere Kombinatoren, die ein Array von Promises entgegennehmen und ein neues Promise zurückgeben. Die Wahl hängt davon ab, wie mit Erfolg und Fehlern umgegangen werden soll:

Methode Erfolgt (resolved), wenn … Schlägt fehl (rejected), wenn … Typischer Einsatz
Promise.all alle Promises erfüllt sind das erste Promise fehlschlägt (Abbruch) Alles-oder-nichts: alle Daten zwingend nötig
Promise.allSettled alle Promises abgeschlossen sind (egal ob Erfolg oder Fehler) nie – liefert für jedes Promise Status und Wert/Grund Teilergebnisse erlaubt, jedes Resultat einzeln auswerten
Promise.race das erste Promise erfüllt wird das erste Promise fehlschlägt – je nachdem, was zuerst eintritt Timeout-Muster, schnellste Antwort gewinnt
Promise.any das erste Promise erfüllt wird alle Promises fehlschlagen (AggregateError) Erster Erfolg zählt, einzelne Fehler ignorieren

Mit Promise.all starten alle Anfragen gleichzeitig, und das Ergebnis ist ein Array mit allen Werten in derselben Reihenfolge:

const [nutzer, beitraege] = await Promise.all([
  fetch("/api/nutzer").then(r => r.json()),
  fetch("/api/beitraege").then(r => r.json())
]);

Der häufigste Fehler: await in Schleifen

Ein klassischer Stolperstein ist await innerhalb einer for-Schleife. Dabei wird jede Aufgabe erst gestartet, nachdem die vorherige fertig ist – die Aufrufe laufen nacheinander statt gleichzeitig. Bei zehn Anfragen mit je 200 Millisekunden ergeben sich so zwei Sekunden statt etwa 200 Millisekunden:

// Langsam: nacheinander
for (const id of ids) {
  const daten = await ladeDaten(id);
}

// Schnell: parallel
const ergebnisse = await Promise.all(
  ids.map(id => ladeDaten(id))
);

Die zweite Variante startet alle Aufrufe sofort und wartet dann gemeinsam auf alle. Sequenzielles await in einer Schleife ist nur dann richtig, wenn ein Schritt wirklich auf das Ergebnis des vorherigen angewiesen ist.

Praktische Faustregeln für den Alltag

Häufige Fragen

Was ist der Unterschied zwischen einem Promise und async/await?

Es gibt keinen funktionalen Unterschied – async/await ist eine bequemere Schreibweise auf Basis von Promises. Jede mit async markierte Funktion gibt ein Promise zurück, und await wartet intern auf dessen Abschluss. Der Code wird dadurch nur lesbarer, ohne dass sich am Verhalten etwas ändert.

Blockiert await die ganze Seite?

Nein. await pausiert ausschließlich die aktuelle async-Funktion. Der restliche JavaScript-Code und die Benutzeroberfläche bleiben reaktionsfähig, weil JavaScript in der Wartezeit andere Aufgaben abarbeitet.

Wann sollte ich Promise.all und wann allSettled nutzen?

Nutzen Sie Promise.all, wenn alle Aufgaben gelingen müssen und ein einzelner Fehler den ganzen Vorgang sinnlos macht. Greifen Sie zu Promise.allSettled, wenn Teilergebnisse in Ordnung sind und Sie jeden Ausgang einzeln auswerten möchten – etwa beim Laden mehrerer optionaler Datenquellen.

Warum ist mein Wert nach einem fetch undefined?

Meist liegt es daran, dass auf das Promise nicht gewartet wurde. Wenn Sie das Ergebnis ohne await verwenden, halten Sie noch das Promise-Objekt in der Hand, nicht den Wert. Achten Sie darauf, await vor den Aufruf zu setzen oder das Promise mit .then() aufzulösen.

Kann ich await außerhalb einer async-Funktion verwenden?

In modernen Modulen ist sogenanntes Top-Level-await erlaubt, also await direkt auf oberster Ebene eines ES-Moduls. In normalen Funktionen oder klassischen Skripten muss await jedoch innerhalb einer async-Funktion stehen, sonst gibt es einen Syntaxfehler.

Was passiert mit Fehlern, die ich nicht abfange?

Ein abgelehntes Promise ohne passenden .catch() oder try/catch erzeugt eine „unhandled promise rejection". Im Browser landet das als Warnung in der Konsole, in Node.js kann es den Prozess beenden. Fangen Sie Fehler deshalb immer bewusst ab.

Wenn Sie diese Grundlagen verinnerlicht haben, fällt der Umgang mit Netzwerkanfragen und anderen langsamen Operationen deutlich leichter. Weitere praktische Helfer für den Entwickleralltag finden Sie in unseren Webtools, regelmäßige Beiträge rund um Technik und Programmierung im News-Bereich, und einen Überblick über alle Angebote auf der Startseite.