17 Fortgeschrittene Konzepte

17.1 Asynchrones JavaScript

Asynchrone Programmierung ist ein fundamentales Konzept in JavaScript, das es ermöglicht, langwierige Operationen durchzuführen, ohne den Hauptausführungsthread zu blockieren. In diesem Abschnitt werden wir die verschiedenen Ansätze für asynchrones JavaScript untersuchen, angefangen bei Callbacks bis hin zu modernen Konzepten wie Promises und async/await.

17.1.1 Das Problem der Synchronität

In synchronem Code werden Anweisungen sequentiell ausgeführt - eine nach der anderen. Wenn eine Operation Zeit benötigt (wie Netzwerkanfragen, Datenbankzugriffe oder Dateisystemoperationen), blockiert sie den kompletten Ausführungsfluss:

// Synchrones Beispiel (zur Veranschaulichung)
function holeDatenVomServer() {
  // Diese Operation würde in der Realität mehrere Sekunden dauern
  return "Daten vom Server";
}

console.log("Anfrage wird gestartet...");
const daten = holeDatenVomServer(); // Hier würde das Programm warten
console.log("Daten erhalten:", daten);
console.log("Weiter im Programm");

In einer browserbasierten Umgebung würde dieser synchrone Ansatz zu einer eingefrorenen Benutzeroberfläche führen, während in Node.js der Server keine weiteren Anfragen bearbeiten könnte.

17.1.2 Callbacks: Der klassische Ansatz

Callbacks waren der erste Mechanismus in JavaScript, um asynchrone Operationen zu handhaben. Ein Callback ist eine Funktion, die als Argument an eine andere Funktion übergeben und aufgerufen wird, sobald die asynchrone Operation abgeschlossen ist.

function holeDatenVomServer(callback) {
  // Simulation einer asynchronen Operation mit setTimeout
  setTimeout(() => {
    const daten = "Daten vom Server";
    callback(daten);
  }, 2000);
}

console.log("Anfrage wird gestartet...");
holeDatenVomServer((daten) => {
  console.log("Daten erhalten:", daten);
});
console.log("Weiter im Programm"); // Diese Zeile wird sofort ausgeführt

Bei diesem Ansatz wird die Ausgabe in folgender Reihenfolge erscheinen: 1. “Anfrage wird gestartet…” 2. “Weiter im Programm” 3. Nach ca. 2 Sekunden: “Daten erhalten: Daten vom Server”

17.1.2.1 Callback-Hölle (Callback Hell)

Ein bekanntes Problem bei der Verwendung von Callbacks ist die sogenannte “Callback-Hölle” oder “Pyramid of Doom”, die entsteht, wenn mehrere asynchrone Operationen verschachtelt werden:

holeDatenVomServer((serverDaten) => {
  verarbeiteDaten(serverDaten, (verarbeiteteDaten) => {
    speichereDaten(verarbeiteteDaten, (ergebnis) => {
      sendeErgebnisAnBenutzer(ergebnis, (antwort) => {
        loggeAktion(antwort, () => {
          console.log("Endlich fertig!");
        });
      });
    });
  });
});

Dieser Ansatz macht den Code schwer lesbar, wartbar und fehleranfällig.

17.1.3 Promises: Strukturierte asynchrone Programmierung

Promises wurden mit ECMAScript 6 (2015) standardisiert und bieten einen eleganten Weg, um asynchrone Operationen zu handhaben. Ein Promise repräsentiert einen zukünftigen Wert, der entweder erfolgreich sein (resolved) oder fehlschlagen (rejected) kann.

function holeDatenVomServer() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Erfolgsfall
      resolve("Daten vom Server");
      
      // Bei einem Fehler würden wir stattdessen reject aufrufen:
      // reject(new Error("Netzwerkfehler"));
    }, 2000);
  });
}

console.log("Anfrage wird gestartet...");
holeDatenVomServer()
  .then((daten) => {
    console.log("Daten erhalten:", daten);
    return daten.toUpperCase();
  })
  .then((verarbeiteteDaten) => {
    console.log("Verarbeitete Daten:", verarbeiteteDaten);
  })
  .catch((fehler) => {
    console.error("Fehler aufgetreten:", fehler);
  })
  .finally(() => {
    console.log("Anfrage abgeschlossen (erfolgreich oder nicht)");
  });

console.log("Weiter im Programm");

17.1.3.1 Promise-Chaining

Ein großer Vorteil von Promises ist die Möglichkeit, sie zu verketten. Dadurch lässt sich die “Callback-Hölle” vermeiden:

holeDatenVomServer()
  .then(verarbeiteDaten)
  .then(speichereDaten)
  .then(sendeErgebnisAnBenutzer)
  .then(loggeAktion)
  .then(() => {
    console.log("Alles erledigt!");
  })
  .catch((fehler) => {
    console.error("Ein Fehler ist aufgetreten:", fehler);
  });

17.1.3.2 Parallele Promise-Verarbeitung

Um mehrere asynchrone Operationen parallel auszuführen, bietet die Promise-API verschiedene Hilfsmethoden:

// Alle Promises müssen erfolgreich sein
Promise.all([
  holeDatenVomServer1(),
  holeDatenVomServer2(),
  holeDatenVomServer3()
])
  .then(([daten1, daten2, daten3]) => {
    console.log("Alle Daten wurden empfangen", daten1, daten2, daten3);
  })
  .catch((fehler) => {
    console.error("Mindestens eine Anfrage ist fehlgeschlagen:", fehler);
  });

// Der schnellste erfolgreiche Promise gewinnt
Promise.race([
  holeDatenVomServer1(),
  holeDatenVomServer2()
])
  .then((schnellsteDaten) => {
    console.log("Erste Daten erhalten:", schnellsteDaten);
  });

// Mit ES2020: Alle Promises werden abgewartet, auch wenn einige fehlschlagen
Promise.allSettled([
  holeDatenVomServer1(),
  holeDatenVomServer2()
])
  .then((ergebnisse) => {
    ergebnisse.forEach((ergebnis) => {
      if (ergebnis.status === 'fulfilled') {
        console.log('Erfolg:', ergebnis.value);
      } else {
        console.log('Fehler:', ergebnis.reason);
      }
    });
  });

17.1.4 Async/Await: Synchroner Stil für asynchronen Code

Mit ECMAScript 2017 wurde async/await eingeführt, eine syntaktische Erweiterung, die auf Promises aufbaut und es ermöglicht, asynchronen Code so zu schreiben, als wäre er synchron.

async function verarbeiteDaten() {
  try {
    console.log("Anfrage wird gestartet...");
    
    // await pausiert die Funktion, bis der Promise erfüllt ist
    const daten = await holeDatenVomServer();
    console.log("Daten erhalten:", daten);
    
    const verarbeiteteDaten = await transformiereDaten(daten);
    console.log("Daten verarbeitet:", verarbeiteteDaten);
    
    const ergebnis = await speichereDaten(verarbeiteteDaten);
    console.log("Daten gespeichert:", ergebnis);
    
    return ergebnis;
  } catch (fehler) {
    console.error("Fehler während der Verarbeitung:", fehler);
    throw fehler; // Weiterleitung des Fehlers
  } finally {
    console.log("Verarbeitung abgeschlossen");
  }
}

// Aufruf der async-Funktion (gibt ein Promise zurück)
verarbeiteDaten()
  .then((ergebnis) => {
    console.log("Finales Ergebnis:", ergebnis);
  })
  .catch((fehler) => {
    console.error("Verarbeitung fehlgeschlagen:", fehler);
  });

console.log("Weiter im Programm");

Die Verwendung von async/await bietet mehrere Vorteile:

17.1.4.1 Parallele Ausführung mit async/await

Auch mit async/await können Operationen parallel ausgeführt werden:

async function holleAlleDaten() {
  try {
    // Starte alle Anfragen parallel
    const versprechenDaten1 = holeDatenVomServer1();
    const versprechenDaten2 = holeDatenVomServer2();
    const versprechenDaten3 = holeDatenVomServer3();
    
    // Warte auf alle Ergebnisse
    const daten1 = await versprechenDaten1;
    const daten2 = await versprechenDaten2;
    const daten3 = await versprechenDaten3;
    
    return [daten1, daten2, daten3];
  } catch (fehler) {
    console.error("Fehler beim Abrufen der Daten:", fehler);
    throw fehler;
  }
}

// Alternativ mit Promise.all
async function holleAlleDatenParallel() {
  try {
    const alleErgebnisse = await Promise.all([
      holeDatenVomServer1(),
      holeDatenVomServer2(),
      holeDatenVomServer3()
    ]);
    
    return alleErgebnisse;
  } catch (fehler) {
    console.error("Mindestens eine Anfrage ist fehlgeschlagen:", fehler);
    throw fehler;
  }
}

17.1.5 Praktisches Beispiel: Datenabruf mit Fetch API

Die Fetch API ist ein modernes Beispiel für Promises in JavaScript. Hier kombinieren wir Fetch mit async/await für einen Datenabruf:

async function holeBenutzerDaten(benutzerId) {
  try {
    // API-Anfrage mit fetch (gibt ein Promise zurück)
    const antwort = await fetch(`https://api.beispiel.de/benutzer/${benutzerId}`);
    
    // Prüfen, ob die Anfrage erfolgreich war
    if (!antwort.ok) {
      throw new Error(`HTTP-Fehler: ${antwort.status}`);
    }
    
    // JSON-Daten extrahieren (gibt ebenfalls ein Promise zurück)
    const daten = await antwort.json();
    return daten;
  } catch (fehler) {
    console.error("Fehler beim Abrufen der Benutzerdaten:", fehler);
    throw fehler;
  }
}

// Verwendung
async function zeigeBenutzerProfil(benutzerId) {
  try {
    const benutzerDaten = await holeBenutzerDaten(benutzerId);
    console.log("Benutzerprofil:", benutzerDaten);
    // Hier würden wir die Daten in der Benutzeroberfläche anzeigen
  } catch (fehler) {
    console.error("Fehler beim Anzeigen des Benutzerprofils:", fehler);
    // Hier würden wir eine Fehlermeldung anzeigen
  }
}

zeigeBenutzerProfil(123);