Tutorial · Einsteiger

JavaScript: async/await richtig verstehen

von André Heuer 7. Oktober 2025 10 Min. Lesezeit

async/await ist der meistgenutzte Weg, asynchronen Code in JavaScript zu schreiben – und gleichzeitig der am häufigsten missverstandene. Die Syntax sieht synchron aus, verhält sich aber asynchron. Wer das Grundmodell nicht versteht, stolpert über Race Conditions, tote Fehlerbehandlungen und unnötig langsame Abläufe. Dieses Tutorial räumt auf.

Das Grundmodell: die Event Loop

JavaScript ist single-threaded: Es gibt genau einen Call Stack, auf dem Code ausgeführt wird. Zeitraubende Operationen (Netzwerk, Timer, Dateien) delegiert die Laufzeitumgebung an die Umgebung – Browser oder Node.js – und läuft weiter. Kommt das Ergebnis zurück, landet ein Callback in der Message Queue und wird ausgeführt, sobald der Stack leer ist.

await pausiert also niemals das ganze Programm – es pausiert nur die aktuelle async-Funktion und gibt die Kontrolle an die Event Loop zurück. Alles andere läuft munter weiter.

async-Funktionen geben immer Promises zurück

javascript
async function holeNutzer(id) {
    return { id, name: 'Monika' };   // kein Promise nötig …
}

holeNutzer(1).then(nutzer => {
    console.log(nutzer);             // … trotzdem: { id: 1, name: 'Monika' }
});

// await entpackt das Promise wieder:
const nutzer = await holeNutzer(1);

Egal was eine async-Funktion zurückgibt – ein Wert, ein Promise, ein geworfener Fehler – nach außen ist es immer ein Promise. Das macht async-Funktionen komponierbar: Jede kann von jeder anderen awaited werden.

Fehlerbehandlung: try/catch statt .catch()

Mit await wird aus asynchronem Fehlerhandling wieder ganz normales try/catch. Aber Achtung: Der Fehler wird nur gefangen, wenn das await innerhalb des try-Blocks steht.

javascript
// ✓ Richtig: await im try-Block
async function ladeProfil(id) {
    try {
        const antwort = await fetch(`/api/nutzer/${id}`);
        if (!antwort.ok) {
            throw new Error(`HTTP ${antwort.status}`);
        }
        return await antwort.json();
    } catch (fehler) {
        console.error('Laden fehlgeschlagen:', fehler);
        return null;
    }
}

// ✗ Falle: await außerhalb – Fehler geht verloren
async function kaputt() {
    const promise = holeDaten();     // Fehler wird hier schon ausgelöst …
    try {
        const daten = await promise;  // … und hier NICHT mehr gefangen
    } catch (e) {
        // kommt nie an → Unhandled Promise Rejection
    }
}

Sequentiell vs. parallel: der häufigste Performance-Fehler

Wer mehrere awaits untereinander schreibt, zwingt sie in eine Warteschlange – selbst wenn sie nichts voneinander wissen. Für unabhängige Operationen gibt es Promise.all().

javascript
// Sequentiell: 3 × 200 ms = ~600 ms Gesamtzeit
const nutzer   = await holeNutzer(1);      // 200 ms
const bestellungen = await holeBestellungen(1); // 200 ms
const adresse  = await holeAdresse(1);    // 200 ms

// Parallel: alle drei starten sofort, ~200 ms Gesamtzeit
const [nutzer, bestellungen, adresse] = await Promise.all([
    holeNutzer(1),
    holeBestellungen(1),
    holeAdresse(1),
]);
Achtung: Promise.all ist all-or-nothing

Lehnt ein einzelnes Promise ab, verwirft Promise.all() alle Ergebnisse. Wenn du Teilergebnisse behalten willst, nutze Promise.allSettled() – es liefert für jedes Promise Status und Wert bzw. Fehler zurück.

Loops: await im forEach funktioniert nicht

Ein Klassiker: forEach kennt kein await – der Callback wird synchron gestartet, die Schleife wartet nicht. Für sequentielles Await nutze eine for…of-Schleife.

javascript
// ✗ Falle: forEach wartet nicht
ids.forEach(async (id) => {
    await verarbeite(id);        // läuft "los", aber die Schleife ist längst fertig
});
console.log('fertig');          // wird VOR der Verarbeitung ausgegeben

// ✓ Sequentiell mit for…of
for (const id of ids) {
    await verarbeite(id);
}
console.log('fertig');          // wirklich erst nach allen Einträgen

// ✓ Parallel, wenn die Reihenfolge egal ist:
await Promise.all(ids.map(id => verarbeite(id)));

Top-Level Await

In ES-Modulen (Browser und Node.js ab 14.8) ist await auch auf oberster Ebene erlaubt – praktisch für Konfigurationsdateien und Startup-Logik:

javascript
// config.mjs – Top-Level Await
const antwort = await fetch('https://api.example.com/config');
export const config = await antwort.json();

Fazit

async/await ist kein Ersatz für das Promise-Modell, sondern syntaktischer Zucker darüber. Wer drei Regeln verinnerlicht, vermeidet die meisten Fallen: await gehört in den try-Block, unabhängige Operationen gehören in Promise.all() und in Loops gehört await in for…of, nie in forEach.

Das könnte dich auch interessieren

Tutorial

PHP 8: Enums, Readonly Properties und Match

PHP 8 hat den Alltag deutlich verändert: Enums ersetten Klassenkonstanten-Wüsten, readonly schützt Objekte vor versehentlicher Mutation und Match macht bedingte Logik lesbar. Ein Praxisüberblick mit Beispielen.

Weiterlesen