20 Async/Await

Async/Await ist eine syntaktische Erweiterung, die mit ECMAScript 2017 (ES8) eingeführt wurde und die asynchrone Programmierung in JavaScript auf eine neue Ebene hebt. Diese Funktionalität baut auf Promises auf, bietet jedoch eine intuitivere, synchron anmutende Syntax, die asynchronen Code lesbarer und wartbarer macht.

20.1 Grundkonzept von Async/Await

Async/Await besteht aus zwei Schlüsselwörtern:

// Eine einfache asynchrone Funktion
async function holeDaten() {
  // Die Ausführung pausiert hier, bis das Promise erfüllt ist
  const antwort = await fetch('https://api.beispiel.de/daten');
  const daten = await antwort.json();
  
  return daten; // Wird automatisch in ein Promise verpackt
}

// Verwendung der asynchronen Funktion
holeDaten()
  .then(daten => {
    console.log('Daten empfangen:', daten);
  })
  .catch(fehler => {
    console.error('Fehler beim Abrufen der Daten:', fehler);
  });

20.2 Vorteile gegenüber reinen Promises

Async/Await bietet mehrere Vorteile gegenüber der direkten Verwendung von Promises:

  1. Syntaktische Klarheit: Der Code ähnelt strukturell synchronem Code
  2. Bessere Fehlerbehandlung: Verwendung von try/catch wie in synchronem Code
  3. Debugging: Einfachere Fehlerbehebung mit klarerem Call Stack
  4. Sequenzklarheit: Die Ausführungsreihenfolge ist offensichtlicher

Vergleichen wir denselben Code mit Promises und mit Async/Await:

// Mit Promises
function holeDatenMitPromises() {
  return fetch('https://api.beispiel.de/benutzer')
    .then(antwort => antwort.json())
    .then(benutzer => {
      return fetch(`https://api.beispiel.de/posts?benutzerId=${benutzer.id}`)
        .then(antwort => antwort.json())
        .then(posts => {
          return { benutzer, posts };
        });
    });
}

// Mit Async/Await
async function holeDatenMitAsyncAwait() {
  const benutzerAntwort = await fetch('https://api.beispiel.de/benutzer');
  const benutzer = await benutzerAntwort.json();
  
  const postsAntwort = await fetch(`https://api.beispiel.de/posts?benutzerId=${benutzer.id}`);
  const posts = await postsAntwort.json();
  
  return { benutzer, posts };
}

Der Async/Await-Code ist flacher, sequentieller und ähnelt strukturell synchronem Code, wodurch er einfacher zu lesen und zu verstehen ist.

20.3 Fehlerbehandlung mit try/catch

Eine der größten Stärken von Async/Await ist die Möglichkeit, den bekannten try/catch-Mechanismus für die Fehlerbehandlung zu verwenden:

async function benutzerDatenLaden(benutzerId) {
  try {
    const antwort = await fetch(`https://api.beispiel.de/benutzer/${benutzerId}`);
    
    if (!antwort.ok) {
      throw new Error(`HTTP-Fehler: ${antwort.status}`);
    }
    
    const benutzer = await antwort.json();
    console.log('Benutzer geladen:', benutzer);
    return benutzer;
  } catch (fehler) {
    console.error('Fehler beim Laden des Benutzers:', fehler);
    // Fehlerbehandlung
    throw fehler; // Optional: Fehler weiterleiten
  } finally {
    console.log('Benutzerladeprozess abgeschlossen');
    // Aufräumarbeiten durchführen
  }
}

Dies ist besonders nützlich, wenn mehrere asynchrone Operationen ausgeführt werden, da ein einzelner try/catch-Block alle Fehler abfangen kann, ohne dass mehrere .catch()-Aufrufe benötigt werden.

20.4 Umgang mit mehreren asynchronen Operationen

20.4.1 Sequentielle Ausführung

async function sequentielleVerarbeitung(benutzerIds) {
  const ergebnisse = [];
  
  for (const id of benutzerIds) {
    // Jeder Benutzer wird nacheinander geladen
    const benutzer = await benutzerDatenLaden(id);
    ergebnisse.push(benutzer);
  }
  
  return ergebnisse;
}

20.4.2 Parallele Ausführung

async function paralleleVerarbeitung(benutzerIds) {
  // Promise.all mit Map und async/await kombinieren
  const promises = benutzerIds.map(id => benutzerDatenLaden(id));
  
  // Alle Promises parallel ausführen
  const ergebnisse = await Promise.all(promises);
  return ergebnisse;
}

// Oder kürzer geschrieben:
async function paralleleVerarbeitungKurz(benutzerIds) {
  return Promise.all(benutzerIds.map(id => benutzerDatenLaden(id)));
}

20.4.3 Kontrollierte parallele Ausführung

Manchmal möchten wir die Anzahl gleichzeitiger Operationen begrenzen:

async function begrenzteParalleleVerarbeitung(benutzerIds, limit = 3) {
  const ergebnisse = [];
  
  // Array in Gruppen der Größe "limit" aufteilen
  for (let i = 0; i < benutzerIds.length; i += limit) {
    const gruppe = benutzerIds.slice(i, i + limit);
    
    // Diese Gruppe parallel verarbeiten
    const gruppenErgebnisse = await Promise.all(
      gruppe.map(id => benutzerDatenLaden(id))
    );
    
    ergebnisse.push(...gruppenErgebnisse);
  }
  
  return ergebnisse;
}

20.5 Async-Funktionen in verschiedenen Kontexten

Async/Await kann in verschiedenen Funktionskontexten verwendet werden:

// Reguläre Funktion
async function regulaereFunktion() {
  return await fetch('https://api.beispiel.de/daten');
}

// Funktionsausdruck
const funktionsAusdruck = async function() {
  return await fetch('https://api.beispiel.de/daten');
};

// Arrow-Funktion
const arrowFunktion = async () => {
  return await fetch('https://api.beispiel.de/daten');
};

// Methode in einer Klasse
class DatenService {
  async holeDaten() {
    return await fetch('https://api.beispiel.de/daten');
  }
  
  // Auch in Getter/Setter (aber selten verwendet)
  async get daten() {
    return await fetch('https://api.beispiel.de/daten');
  }
}

// In einer IIFE (Immediately Invoked Function Expression)
(async () => {
  try {
    const daten = await fetch('https://api.beispiel.de/daten');
    console.log(await daten.json());
  } catch (fehler) {
    console.error(fehler);
  }
})();

20.6 Top-Level Await (ECMAScript 2022)

Mit ECMAScript 2022 wurde “Top-Level Await” eingeführt, das die Verwendung von await außerhalb von async-Funktionen in ES-Modulen ermöglicht:

// In einem ES-Modul, kein async erforderlich
const antwort = await fetch('https://api.beispiel.de/konfiguration');
const config = await antwort.json();

export const API_KEY = config.apiKey;
export const API_URL = config.apiUrl;

// Abhängigkeiten können auf die Auflösung warten
console.log('Konfiguration geladen');

Dies ist besonders nützlich für: - Initialisierungscode in Modulen - Dynamischen Import von Modulen - Ressourcenladen beim Start - Konfigurationsabhängige Exporte

Beachten Sie, dass Top-Level Await nur in ES-Modulen (mit type="module" oder .mjs-Dateien) und nicht in CommonJS-Modulen oder Skripten funktioniert.

20.7 Praktische Beispiele für Async/Await

20.7.1 Beispiel 1: Daten laden und verarbeiten

async function ladeUndVerarbeiteDaten() {
  try {
    // Daten laden
    const antwort = await fetch('https://api.beispiel.de/daten');
    const rohDaten = await antwort.json();
    
    // Daten verarbeiten
    const verarbeitet = rohDaten.map(item => ({
      id: item.id,
      name: item.name.toUpperCase(),
      wert: item.wert * 2
    }));
    
    // Ergebnis zurückgeben
    return verarbeitet;
  } catch (fehler) {
    console.error('Fehler bei der Datenverarbeitung:', fehler);
    return []; // Fallback-Wert
  }
}

20.7.2 Beispiel 2: Bedingte asynchrone Logik

async function intelligenteDatenBeschaffung(id, { useCache = true } = {}) {
  // Prüfen, ob Daten im Cache sind
  if (useCache) {
    try {
      const cachedData = await ladeAusCache(id);
      if (cachedData) {
        console.log('Daten aus Cache geladen');
        return cachedData;
      }
    } catch (cacheError) {
      console.warn('Cache-Zugriff fehlgeschlagen:', cacheError);
      // Cache-Fehler ignorieren und fortfahren
    }
  }
  
  // Wenn nicht im Cache oder Cache deaktiviert, vom Server laden
  try {
    console.log('Lade Daten vom Server...');
    const serverData = await ladeVomServer(id);
    
    // Im Cache speichern für zukünftige Anfragen
    if (useCache) {
      await speichereImCache(id, serverData);
    }
    
    return serverData;
  } catch (serverError) {
    console.error('Serverzugriff fehlgeschlagen:', serverError);
    throw serverError;
  }
}

20.7.3 Beispiel 3: Wiederholungslogik (Retry Logic)

async function mitWiederholung(funktion, { versuche = 3, verzoegerung = 1000 } = {}) {
  let letzterFehler;
  
  for (let versuch = 1; versuch <= versuche; versuch++) {
    try {
      return await funktion();
    } catch (fehler) {
      letzterFehler = fehler;
      console.warn(`Versuch ${versuch}/${versuche} fehlgeschlagen:`, fehler);
      
      if (versuch < versuche) {
        // Warte vor dem nächsten Versuch
        // Exponentielle Backoff-Strategie
        const wartezeitMs = verzoegerung * Math.pow(2, versuch - 1);
        console.log(`Warte ${wartezeitMs}ms vor dem nächsten Versuch...`);
        await new Promise(resolve => setTimeout(resolve, wartezeitMs));
      }
    }
  }
  
  throw new Error(`Nach ${versuche} Versuchen fehlgeschlagen: ${letzterFehler}`);
}

// Verwendung
async function datenLaden() {
  const daten = await mitWiederholung(
    () => fetch('https://api.beispiel.de/unstable-endpoint').then(r => r.json()),
    { versuche: 5, verzoegerung: 500 }
  );
  
  console.log('Daten erfolgreich geladen:', daten);
}

20.8 Fortgeschrittene Techniken mit Async/Await

20.8.1 Abbruch von asynchronen Operationen mit AbortController

async function ladeDatenMitTimeout(url, timeoutMs = 5000) {
  // AbortController erstellen
  const controller = new AbortController();
  const { signal } = controller;
  
  // Timeout-Promise erstellen
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      controller.abort();
      reject(new Error(`Zeitüberschreitung nach ${timeoutMs}ms`));
    }, timeoutMs);
  });
  
  try {
    // Race zwischen Fetch und Timeout
    const antwort = await Promise.race([
      fetch(url, { signal }),
      timeoutPromise
    ]);
    
    return await antwort.json();
  } catch (fehler) {
    if (fehler.name === 'AbortError') {
      throw new Error('Die Anfrage wurde abgebrochen');
    }
    throw fehler;
  }
}

20.8.2 Asynchrone Iteratoren

Mit ECMAScript 2018 wurden asynchrone Iteratoren eingeführt, die gut mit Async/Await zusammenarbeiten:

async function* generiereDaten() {
  let id = 1;
  while (id <= 5) {
    // Simuliere asynchrone Operationen
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield { id, wert: `Wert ${id}` };
    id++;
  }
}

async function verarbeiteDatenStream() {
  const generator = generiereDaten();
  
  for await (const daten of generator) {
    console.log('Verarbeite:', daten);
    // Weitere Verarbeitung...
  }
  
  console.log('Alle Daten verarbeitet');
}

20.8.3 Asynchrone Funktionen als Event-Handler

Async-Funktionen können als Event-Handler verwendet werden, wobei Vorsicht geboten ist, da die Rückgabewerte (Promises) nicht vom Event-System verarbeitet werden:

document.getElementById('submit-button').addEventListener('click', async (event) => {
  event.preventDefault();
  
  try {
    const formDaten = new FormData(document.getElementById('mein-formular'));
    const antwort = await fetch('/api/submit', {
      method: 'POST',
      body: formDaten
    });
    
    if (!antwort.ok) {
      throw new Error(`Serverfehler: ${antwort.status}`);
    }
    
    const ergebnis = await antwort.json();
    zeigeBestaetigungsMeldung(`Formular erfolgreich übermittelt: ${ergebnis.id}`);
  } catch (fehler) {
    console.error('Fehler bei der Formular-Übermittlung:', fehler);
    zeigeFehlerMeldung(fehler.message);
  }
});

20.9 Häufige Fallstricke bei Async/Await

20.9.1 Fallstrick 1: Vergessen des await-Schlüsselworts

async function fehlerhafte() {
  try {
    // FEHLER: await vergessen
    const antwort = fetch('https://api.beispiel.de/daten');
    const daten = antwort.json(); // Fehler, da antwort ein Promise ist
    
    return daten;
  } catch (fehler) {
    // Dieser catch-Block wird den Fehler NICHT abfangen
    console.error(fehler);
  }
}

// Besser:
async function korrekte() {
  try {
    const antwort = await fetch('https://api.beispiel.de/daten');
    const daten = await antwort.json();
    
    return daten;
  } catch (fehler) {
    console.error(fehler);
    throw fehler;
  }
}

20.9.2 Fallstrick 2: Unbeabsichtigte sequentielle Ausführung

async function ineffizient() {
  // Sequentielle Ausführung, obwohl die Anfragen unabhängig sind
  const benutzer = await holeBenutzerdaten();
  const produkte = await holeProduktdaten();
  
  return { benutzer, produkte };
}

// Besser (parallel):
async function effizient() {
  // Starte beide Anfragen gleichzeitig
  const benutzerPromise = holeBenutzerdaten();
  const produktePromise = holeProduktdaten();
  
  // Warte auf beide Ergebnisse
  const benutzer = await benutzerPromise;
  const produkte = await produktePromise;
  
  return { benutzer, produkte };
}

// Oder noch kürzer:
async function effizientKurz() {
  const [benutzer, produkte] = await Promise.all([
    holeBenutzerdaten(),
    holeProduktdaten()
  ]);
  
  return { benutzer, produkte };
}

20.9.3 Fallstrick 3: Async-Funktionen in for-Schleifen

// PROBLEM: Unbeabsichtigte sequentielle Ausführung in einer Schleife
async function sequentielleVerarbeitung(ids) {
  const ergebnisse = [];
  
  for (const id of ids) {
    const daten = await holeDaten(id); // Sequentiell: Jeder holeDaten-Aufruf wartet auf den vorherigen
    ergebnisse.push(daten);
  }
  
  return ergebnisse;
}

// PROBLEM: Falsche Verwendung mit forEach
async function falscheParallelverarbeitung(ids) {
  const ergebnisse = [];
  
  // ACHTUNG: forEach wartet NICHT auf asynchrone Callbacks
  ids.forEach(async (id) => {
    const daten = await holeDaten(id);
    ergebnisse.push(daten); // Wird möglicherweise ausgeführt, nachdem die Funktion bereits zurückgekehrt ist
  });
  
  return ergebnisse; // Gibt ein leeres Array zurück, bevor die asynchronen Operationen abgeschlossen sind
}

// BESSER: Parallele Ausführung mit Promise.all und map
async function korrekteParallelverarbeitung(ids) {
  const ergebnisse = await Promise.all(
    ids.map(async (id) => {
      return await holeDaten(id);
    })
  );
  
  return ergebnisse;
}

20.9.4 Fallstrick 4: Fehlerbehandlung außerhalb des try/catch-Blocks

async function unvollstaendigeFehlerbehandlung() {
  try {
    const antwort = await fetch('https://api.beispiel.de/daten');
    const daten = await antwort.json();
    return daten;
  } catch (fehler) {
    console.error('Fehler beim Abrufen der Daten:', fehler);
    return []; // Fallback-Wert
  }
  
  // Code hier wird nie erreicht, wenn ein Fehler auftritt
  await protokolliere('Daten abgerufen'); // Diese Zeile wird übersprungen, wenn ein Fehler auftritt
}

// Besser:
async function vollstaendigeFehlerbehandlung() {
  try {
    const antwort = await fetch('https://api.beispiel.de/daten');
    const daten = await antwort.json();
    
    // Protokollierung innerhalb des try-Blocks
    await protokolliere('Daten abgerufen');
    
    return daten;
  } catch (fehler) {
    console.error('Fehler beim Abrufen der Daten:', fehler);
    
    // Separate Protokollierung für den Fehlerfall
    await protokolliere('Fehler beim Datenabruf');
    
    return []; // Fallback-Wert
  }
}

20.10 Async/Await in verschiedenen Umgebungen

20.10.1 Browser-Umgebung

// In einem modernen Browser
async function ladeFrontendDaten() {
  // Fetch API
  const antwort = await fetch('/api/daten');
  const daten = await antwort.json();
  
  // DOM-Manipulation
  document.getElementById('ergebnis').textContent = daten.nachricht;
  
  // Web Storage API
  localStorage.setItem('letztesDatum', new Date().toISOString());
  
  // Weitere Browser-APIs
  const erlaubnis = await Notification.requestPermission();
  if (erlaubnis === 'granted') {
    new Notification('Daten aktualisiert');
  }
}

20.10.2 Node.js-Umgebung

// In Node.js
const fs = require('fs').promises;
const { promisify } = require('util');
const childProcess = require('child_process');
const exec = promisify(childProcess.exec);

async function backendVerarbeitung() {
  // Dateisystem-Operationen
  const konfigDaten = await fs.readFile('config.json', 'utf8');
  const config = JSON.parse(konfigDaten);
  
  // Externe Prozesse ausführen
  const { stdout } = await exec('git status');
  console.log('Git Status:', stdout);
  
  // HTTP-Anfragen
  const response = await fetch('https://api.extern.de/status');
  const apiStatus = await response.json();
  
  // Ergebnisse kombinieren und speichern
  const ergebnis = {
    config,
    gitStatus: stdout,
    apiStatus
  };
  
  await fs.writeFile('status-report.json', JSON.stringify(ergebnis, null, 2));
  return ergebnis;
}