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.
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.
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ührtBei 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”
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.
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");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);
});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);
}
});
});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:
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;
}
}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);