Promises in JavaScript: Asynchronen Code sauber verketten
Callbacks werden schnell unübersichtlich. Promises bringen Struktur in asynchronen Code – mit then, catch und finally. So nutzt du sie richtig.
Sobald deine Anwendung Daten aus dem Netzwerk lädt, auf Timer wartet oder eine Datei liest, passiert etwas asynchron: Der Code läuft nicht sofort durch, sondern das Ergebnis kommt erst später. Früher hat man solche Abläufe mit verschachtelten Callbacks gelöst – und ist damit oft in der berüchtigten "Callback-Hölle" gelandet. Promises sind die moderne Antwort darauf. In diesem Beitrag lernst du, wie sie funktionieren und wie du sie sauber verkettest.
Was ist ein Promise überhaupt?
Ein Promise ist ein Objekt, das für einen Wert steht, der vielleicht jetzt noch nicht, aber irgendwann in der Zukunft verfügbar sein wird. Du kannst dir das wie einen Abholschein vorstellen: Du hast noch nicht die Ware in der Hand, aber ein Versprechen, dass du sie bekommst – oder eine Absage.
Ein Promise befindet sich immer in genau einem von drei Zuständen:
- pending – das Ergebnis steht noch aus.
- fulfilled – die Operation war erfolgreich, es gibt einen Wert.
- rejected – die Operation ist fehlgeschlagen, es gibt einen Fehler.
Sobald ein Promise erfüllt oder abgelehnt wurde, ändert sich sein Zustand nicht mehr. Man sagt, es ist dann settled (abgeschlossen).
Ein Promise erstellen
Meistens bekommst du Promises von APIs geliefert (etwa von fetch). Aber es hilft zu verstehen, wie man sie selbst baut. Der Konstruktor bekommt eine Funktion mit zwei Parametern: resolve für den Erfolgsfall und reject für den Fehlerfall.
function ladeDaten(erfolgreich) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (erfolgreich) {
resolve({ id: 1, name: "Anna" });
} else {
reject(new Error("Daten konnten nicht geladen werden"));
}
}, 1000);
});
}Die Funktion gibt sofort ein Promise im Zustand pending zurück. Nach einer Sekunde ruft der Timer entweder resolve oder reject auf – und erst dann steht der Zustand fest.
Auf das Ergebnis reagieren mit then, catch und finally
Um an den Wert eines Promise zu kommen, hängst du mit .then() eine Funktion an. Sie wird aufgerufen, sobald das Promise erfüllt ist. Fehler fängst du mit .catch() ab, und .finally() läuft in jedem Fall – egal ob Erfolg oder Fehler.
ladeDaten(true)
.then((user) => {
console.log("Erfolg:", user.name); // "Erfolg: Anna"
})
.catch((fehler) => {
console.error("Fehler:", fehler.message);
})
.finally(() => {
console.log("Ladevorgang abgeschlossen");
});Wichtig: Ein .catch() fängt nicht nur direkte Rejections ab, sondern auch Fehler, die in einem vorherigen .then() geworfen werden. Es reicht also oft, ein einziges catch ans Ende der Kette zu hängen.
Promises verketten statt verschachteln
Der eigentliche Charme von Promises zeigt sich beim Verketten. Gibst du in einem .then() ein neues Promise zurück, wartet die Kette automatisch auf dessen Ergebnis, bevor der nächste .then()-Block läuft. So bleibt der Code flach und lesbar – ganz ohne tiefe Verschachtelung.
function holeUser(id) {
return fetch(`https://api.example.com/users/${id}`)
.then((antwort) => {
if (!antwort.ok) {
throw new Error(`HTTP-Fehler: ${antwort.status}`);
}
return antwort.json(); // gibt ein Promise zurück
})
.then((user) => {
// Ergebnis des vorherigen Promise steht hier bereit
return fetch(`https://api.example.com/posts?user=${user.id}`);
})
.then((antwort) => antwort.json())
.then((posts) => {
console.log(`Der User hat ${posts.length} Beiträge`);
});
}Jeder Schritt gibt seinen Wert an den nächsten weiter. Ein einziges .catch() am Ende würde jeden Fehler aus dieser Kette auffangen.
Mehrere Promises parallel: Promise.all und Co.
Oft möchtest du mehrere unabhängige Operationen gleichzeitig starten, statt sie nacheinander abzuarbeiten. Dafür gibt es hilfreiche statische Methoden:
Promise.all– wartet, bis alle Promises erfüllt sind. Schlägt eines fehl, schlägt das Ganze fehl.Promise.allSettled– wartet auf alle und liefert für jedes das Ergebnis, egal ob erfüllt oder abgelehnt.Promise.race– gibt das Ergebnis des ersten abgeschlossenen Promise zurück.
const urls = [
"https://api.example.com/users/1",
"https://api.example.com/users/2",
"https://api.example.com/users/3",
];
Promise.all(urls.map((url) => fetch(url).then((r) => r.json())))
.then((users) => {
console.log(`${users.length} User parallel geladen`);
})
.catch((fehler) => {
console.error("Mindestens eine Anfrage schlug fehl:", fehler.message);
});Weil die drei fetch-Aufrufe gleichzeitig starten, ist Promise.all deutlich schneller, als die Anfragen einzeln hintereinander zu erledigen.
async/await: Promises noch lesbarer
Seit ES2017 gibt es mit async und await eine Syntax, die auf Promises aufbaut, aber wie normaler, synchroner Code aussieht. Ein await pausiert die Funktion, bis das Promise abgeschlossen ist – und gibt dessen Wert zurück. Fehler behandelst du mit try/catch.
async function zeigeUser(id) {
try {
const antwort = await fetch(`https://api.example.com/users/${id}`);
if (!antwort.ok) {
throw new Error(`HTTP-Fehler: ${antwort.status}`);
}
const user = await antwort.json();
console.log("Geladen:", user.name);
return user;
} catch (fehler) {
console.error("Konnte User nicht laden:", fehler.message);
}
}Unter der Haube arbeitet await weiterhin mit Promises – es ist also kein Ersatz, sondern eine bequemere Schreibweise. Wer Promises verstanden hat, versteht auch async/await.
Fazit
Promises geben dir ein sauberes, verkettbares Werkzeug, um mit asynchronem Code umzugehen. Statt Callbacks tief zu verschachteln, reihst du klare Schritte mit .then() aneinander, fängst Fehler zentral mit .catch() ab und startest unabhängige Aufgaben parallel mit Promise.all. Und wenn du es noch lesbarer willst, greifst du zu async/await – das im Kern ebenfalls nur Promises verwendet. Am besten übst du direkt an einer echten API und beobachtest in der Konsole, in welcher Reihenfolge deine Schritte tatsächlich laufen.