Formulare, API-Calls, Animationen: Moderne Webanwendungen bestehen zu einem großen Teil aus asynchronem Code. Wer hier nur mit klassischen Callbacks arbeitet, landet schnell im Chaos. JavaScript Promises lösen genau dieses Problem – vorausgesetzt, die Funktionsweise ist wirklich verstanden.
Der Artikel führt praxisnah durch die Grundlagen von Promises, typische Muster, Fehlerquellen und zeigt, wie sich mit async/await lesbarer Code schreiben lässt.
JavaScript Promises Grundlagen – was sie sind und warum sie helfen
Ein Promise ist ein Objekt, das ein Ergebnis verspricht, das jetzt noch nicht da ist. Es steht für einen zukünftigen Wert: erfolgreich (fulfilled), fehlgeschlagen (rejected) oder noch ausstehend (pending).
Promise-Zustände einfach erklärt
Ein Promise durchläuft drei zentrale Zustände:
- pending – die asynchrone Operation läuft noch
- fulfilled – die Operation war erfolgreich, es gibt ein Ergebnis
- rejected – es ist ein Fehler passiert, es gibt einen Fehlergrund
Die Stärke liegt darin, dass sich mit diesen Zuständen eine klare Kette von Reaktionen aufbauen lässt – statt verschachtelter Callback-Pyramiden.
Vergleich: Callback vs. Promise auf einen Blick
| Aspekt | Callback | Promise |
|---|---|---|
| Fehlerbehandlung | Oft verstreut in vielen Funktionen | zentral mit .catch() oder try/catch |
| Lesbarkeit | Schnell verschachtelt (Callback-Hölle) | lineare Ketten, gut nachvollziehbar |
| Komposition | Manuell, fehleranfällig | Hilfsfunktionen wie Promise.all() |
Promise erstellen und auflösen – Schritt für Schritt
Um Promises wirklich zu beherrschen, hilft es, sie einmal selbst von Hand zu bauen. Das klärt viele Fragen, die später bei Frameworks oder Fetch-APIs auftauchen.
Eigenes Promise mit resolve und reject schreiben
Ein Promise wird mit einem Konstruktor erstellt, der eine sogenannte Executor-Funktion erhält. Diese bekommt zwei Funktionen übergeben: resolve für Erfolg und reject für Fehler.
Beispiel für ein einfaches Timeout-Promise:
const wait = (ms) => new Promise((resolve, reject) => {
if (ms < 0) {
reject(new Error('Zeit muss positiv sein'));
return;
}
setTimeout(() => {
resolve(ms);
}, ms);
});
Dieses Promise löst sich nach einer bestimmten Zeit auf. Wer es nutzt, muss sich nicht mehr darum kümmern, wie setTimeout intern arbeitet.
then, catch und finally richtig verwenden
Ist ein Promise erstellt, wird das Ergebnis mit Methoden abonniert:
.then(onFulfilled)– reagiert auf Erfolg.catch(onRejected)– reagiert auf Fehler.finally(onFinally)– läuft immer, egal ob Erfolg oder Fehler
Ein praktischer Aufruf mit dem wait-Beispiel:
wait(1000)
.then((ms) => {
console.log(`Wartezeit: ${ms} ms`);
})
.catch((error) => {
console.error('Fehler:', error.message);
})
.finally(() => {
console.log('Fertig');
});
Wichtig zu verstehen: Jede .then()– und .catch()-Methode erzeugt selbst wieder ein Promise. So entstehen Ketten, in denen sich Daten sauber Schritt für Schritt verarbeiten lassen.
Promise-Ketten aufbauen – Datenfluss statt Callback-Hölle
Mit Promises lassen sich Abläufe klar in Schritte zerlegen: Daten holen, transformieren, validieren, anzeigen. Jeder Schritt wird ein eigenes .then().
Praxisbeispiel: API-Daten nacheinander verarbeiten
Ein typisches Szenario: Daten aus einer API laden, prüfen und im UI anzeigen. Die native fetch-Funktion liefert bereits ein Promise zurück.
fetch('/api/user')
.then((response) => {
if (!response.ok) {
throw new Error('Request fehlgeschlagen');
}
return response.json();
})
.then((user) => {
if (!user.name) {
throw new Error('Name fehlt');
}
return user;
})
.then((user) => {
renderUser(user);
})
.catch((error) => {
showError(error.message);
});
Hier wird sichtbar, wie ein Fehler in einem frühen Schritt (etwa fehlender Name) später zentral im .catch() landet. Diese Struktur zahlt direkt auf gut wartbaren Code ein – ein Thema, das auch bei Clean Code in JavaScript eine Rolle spielt.
Typische Fehler in Promise-Ketten vermeiden
- Fehlendes return: Wird in einem
.then()vergessen, landet im nächsten.then()undefinedstatt der gewünschten Daten. - Fehler im
.then()werden ignoriert: Nur ein.catch()am Ende fängt alle Fehler ab. - Promise in Callback verstecken: Statt eine Kette durchzuziehen, wird wieder ein verschachtelter Stil eingeführt – das macht Debugging unnötig schwer.
async und await – Promises in lesbaren Code übersetzen
async/await ist keine Alternative zu Promises, sondern eine andere Syntax dafür. Intern laufen weiterhin Promises, die Schreibweise wirkt aber wie synchroner Code.
async-Funktion und Rückgabewerte verstehen
Eine Funktion, die mit async markiert ist, gibt automatisch ein Promise zurück. Der Rückgabewert der Funktion wird zur aufgelösten Promise.
async function loadUser() {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error('Request fehlgeschlagen');
}
const user = await response.json();
return user;
}
Beim Aufruf kann weiter per .then() gearbeitet werden – oder die Funktion wird wiederum mit await verwendet.
try/catch für asynchrone Fehlerbehandlung einsetzen
Mit async/await lässt sich Fehlerbehandlung wieder über try/catch lösen, wie bei synchronem Code:
async function showUser() {
try {
const user = await loadUser();
renderUser(user);
} catch (error) {
showError(error.message);
}
}
Das macht komplexe Abläufe deutlich lesbarer. Gute Fehlerbehandlung ist dabei genauso wichtig wie beim Thema strukturierte JavaScript-Fehlerbehandlung.
async/await und Parallelität mit Promise.all kombinieren
Mehrere unabhängige Requests können parallel ausgeführt werden, statt nacheinander zu warten. Dafür eignet sich Promise.all() in Kombination mit await:
async function loadDashboard() {
const [user, stats] = await Promise.all([
fetch('/api/user').then((r) => r.json()),
fetch('/api/stats').then((r) => r.json()),
]);
renderDashboard(user, stats);
}
Wichtig: Schlägt eine der Promises fehl, wird das gesamte Promise.all() abgelehnt und der Fehler muss entsprechend behandelt werden.
Promise.all, race und weitere Hilfsfunktionen im Überblick
Neben Promise.all() bietet die Sprache weitere Helfer, um mehrere asynchrone Vorgänge zu koordinieren. Sie sind besonders in komplexeren Architekturen hilfreich, etwa bei Microservices oder bei der Arbeit mit mehreren unabhängigen APIs.
Promise.all vs. Promise.allSettled
Promise.all()wartet auf alle Promises und wird verworfen, sobald eines scheitert.Promise.allSettled()wartet auf alle Promises und liefert den Status jeder einzelnen Operation zurück – inklusive Fehlermeldung.
Ein Anwendungsfall für allSettled sind Dashboards, bei denen möglichst viele Kacheln gefüllt werden sollen, auch wenn einzelne Datenquellen ausfallen.
Promise.race und Timeouts umsetzen
Promise.race() löst sich mit dem Ergebnis des zuerst abgeschlossenen Promises auf – Erfolg oder Fehler ist egal. Das eignet sich gut für Timeouts.
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
return Promise.race([promise, timeout]);
}
So lässt sich ein beliebiger API-Call mit einer maximalen Wartezeit kombinieren, ohne die eigentliche Logik zu verändern.
So geht’s: Promises systematisch im Projekt etablieren
Wer mit einem bestehenden Codebestand arbeitet, steht oft vor der Frage: Wo anfangen? Eine schrittweise Umstellung hilft, Risiken zu minimieren.
Checkliste für saubere Async-Architektur
- Alle asynchronen Stellen im Projekt identifizieren (API-Calls, Timer, Event-Listener).
- Neue Funktionen grundsätzlich als Promise-basierte Schnittstellen planen.
- Wichtige Abläufe zuerst von Callbacks auf Promises umstellen.
- Für komplexere Sequenzen gezielt async/await einsetzen, um Lesbarkeit zu erhöhen.
- Einheitliche Fehlerstrategie definieren (zentrale
.catch()odertry/catchmit Logging). - Promise-Helfer wie
Promise.all()bewusst nutzen, um Parallelität auszuschöpfen.
Häufige Fragen zu JavaScript Promises
Wann Promise, wann async/await verwenden?
Im Kern geht es um Stil-Entscheidungen. Für kurze, lineare Abläufe sind Promise-Ketten mit .then() oft ausreichend. Sobalds mehrere Schritte, Bedingungen und Abzweigungen gibt, bietet async/await meist besser lesbaren Code. Technisch basieren beide Varianten auf derselben Mechanik.
Müssen alle alten Callbacks umgeschrieben werden?
Nein. Bestehende, stabile Callback-APIs können im Projekt bleiben. Es lohnt sich aber, neue Schnittstellen konsequent Promise-basiert zu gestalten. Bei Bedarf lassen sich alte Funktionen mit sogenannten „Promisify“-Hilfen in Promises einwickeln, ohne die Implementierung komplett umzubauen.
Wie wirken sich Promises auf Performance aus?
Promises sind in modernen JavaScript-Engines effizient umgesetzt. In typischen Webanwendungen fällt ihre Overhead im Vergleich zur Netzwerk-Latenz oder Datenbank-Abfragen kaum ins Gewicht. Entscheidend ist eine klare Struktur – ähnlich wie bei durchdachtem Event-Handling, wie es im Beitrag JavaScript Event-Handling verstehen beschrieben wird.
Mini-Fallbeispiel: Von Callbacks zu Promises im Login-Flow
Ein Team betreibt eine Single-Page-App mit Login-Flow, der aus mehreren verschachtelten Callbacks besteht: Daten prüfen, Session anlegen, Benutzerprofil laden, Weiterleitung. Fehler häufen sich, weil an verschiedenen Stellen unterschiedlich reagiert wird.
Im Refactoring-Schritt wird ein zentraler Login-Service eingeführt, der ein Promise zurückgibt. Validierung, Session-Handling und Profil-Laden werden darin gekapselt und mit async/await umgesetzt. Fehler laufen gesammelt in einem try/catch zusammen, wo Logging und Nutzer-Feedback geregelt werden. Ergebnis: deutlich weniger Seiteneffekte, klarer Datenfluss und weniger Aufwand bei zukünftigen Anpassungen.

