Promises & Async/Await in JavaScript
JavaScript Promises und async/await verständlich erklärt: Von Callback Hell zu modernem asynchronem Code. Mit Praxisbeispielen und Best Practices.
JavaScript ist Single-Threaded – es kann nur eine Aufgabe gleichzeitig ausführen. Trotzdem können wir asynchrone Operationen wie API-Calls, Datei-Zugriffe oder Timer parallel verarbeiten. Das Geheimnis: Promises und der Event Loop. Wer Promises versteht, versteht einen der fundamentalsten Bausteine moderner Web-Entwicklung.
Bevor es Promises gab: Callback Hell
Vor ES6 (2015) war die einzige Möglichkeit für asynchronen Code: Callbacks.
// "Callback Hell" – verschachtelte Callbacks
getUser(1, function(user) {
getPostsByUser(user.id, function(posts) {
getCommentsForPosts(posts, function(comments) {
renderPage(user, posts, comments, function() {
console.log('Seite geladen!');
});
});
});
});
Der Code wächst nach rechts, Fehlerbehandlung ist schwierig, und die Lesbarkeit leidet massiv. Promises lösen genau dieses Problem.
Was ist ein Promise?
Ein Promise ist ein Objekt, das einen zukünftigen Wert repräsentiert. Man kann sich ein Promise wie eine Bestellnummer vorstellen: Du bekommst sie sofort, aber das Ergebnis kommt erst später.
Die drei Zustände
┌─────────────┐
│ pending │ ← Initialer Zustand
└──────┬──────┘
│
├────────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ fulfilled │ │ rejected │
│ (Erfolg) │ │ (Fehler) │
└─────────────┘ └─────────────┘
└────────────────┘
settled
- pending: Die Operation läuft noch
- fulfilled: Erfolgreich abgeschlossen – ein Wert liegt vor
- rejected: Fehlgeschlagen – ein Fehlergrund liegt vor
Ein Promise kann seinen Zustand nur einmal wechseln. Danach bleibt er fixiert.
Promise erstellen
const meineOperation = new Promise((resolve, reject) => {
const erfolg = true;
if (erfolg) {
resolve('Daten erfolgreich geladen!');
} else {
reject(new Error('Etwas ist schiefgelaufen'));
}
});
In der Praxis erstellt man selten Promises von Hand. Meistens nutzt man APIs die bereits Promises zurückgeben – fetch(), File-APIs und viele mehr.
Wann erstellt man eigene Promises?
Eigene Promises sind nützlich, um Callback-basierte APIs zu “promisifizieren”:
// setTimeout als Promise
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(2000);
console.log('2 Sekunden vergangen');
// Geolocation als Promise
function getPosition() {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
}
const position = await getPosition();
console.log(position.coords.latitude);
Promises konsumieren
Mit .then() und .catch()
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => {
console.log('Daten:', data);
})
.catch(error => {
console.error('Fehler:', error);
})
.finally(() => {
// Wird IMMER ausgeführt – ideal für Aufräumarbeiten
hideLoadingSpinner();
});
Chaining – Verkettung
Jedes .then() gibt ein neues Promise zurück:
getUserById(1)
.then(user => getPostsByUser(user.id))
.then(posts => getCommentsForPosts(posts))
.then(comments => {
console.log('Alle Kommentare:', comments);
})
.catch(error => {
// Fängt Fehler aus ALLEN vorherigen Schritten
console.error('Fehler in der Kette:', error);
});
Async/Await – Die moderne Syntax
async/await (ES2017) macht asynchronen Code lesbar wie synchronen Code. Es ist syntaktischer Zucker über Promises.
async function loadUserData(userId) {
try {
const user = await getUserById(userId);
const posts = await getPostsByUser(user.id);
const comments = await getCommentsForPosts(posts);
return { user, posts, comments };
} catch (error) {
console.error('Fehler:', error);
throw error;
}
}
const data = await loadUserData(1);
Wichtige Regeln
awaitfunktioniert nur inasyncFunktionen (oder Top-Level in ES Modules)awaitpausiert die Funktion, nicht den gesamten Thread- Eine
asyncFunktion gibt immer ein Promise zurück
// async Arrow Functions
const fetchUser = async (id) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
};
// async Methoden in Klassen
class UserService {
async getById(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
}
// Top-level await (in ES Modules)
const config = await fetch('/config.json').then(r => r.json());
Parallele Ausführung
Einer der häufigsten Performance-Fehler: Requests sequenziell statt parallel absetzen.
Promise.all() – Alle müssen erfolgreich sein
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
// Wenn EINER fehlschlägt, schlägt ALLES fehl!
Promise.allSettled() – Ergebnisse unabhängig vom Status
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/broken-endpoint'), // Schlägt fehl
fetch('/api/posts')
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Request ${index}: Erfolg`, result.value);
} else {
console.log(`Request ${index}: Fehler`, result.reason);
}
});
Ideal für Dashboard-Widgets, die unabhängig voneinander laden.
Promise.race() – Der Schnellste gewinnt
// Timeout-Pattern: Request mit Zeitlimit
const result = await Promise.race([
fetch('https://api.example.com/data'),
delay(5000).then(() => {
throw new Error('Request Timeout nach 5 Sekunden');
})
]);
Promise.any() – Erster Erfolg gewinnt
const firstSuccess = await Promise.any([
fetch('https://primary.com/data'),
fetch('https://fallback1.com/data'),
fetch('https://fallback2.com/data')
]);
// Wirft AggregateError nur wenn ALLE fehlschlagen
Vergleich der Promise-Methoden
┌─────────────────────┬───────────────────────┬─────────────────────────────┐
│ Methode │ Wartet auf │ Fehlschlag wenn │
├─────────────────────┼───────────────────────┼─────────────────────────────┤
│ Promise.all() │ Alle │ Ein Promise rejected │
│ Promise.allSettled()│ Alle │ Nie (sammelt alle) │
│ Promise.race() │ Erstes (egal ob ok) │ Erstes ist rejected │
│ Promise.any() │ Erstes erfolgreiches │ Alle rejected │
└─────────────────────┴───────────────────────┴─────────────────────────────┘
Error Handling Best Practices
Pattern 1: Try-Catch mit Fehlertyp-Unterscheidung
async function safeFetch(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (error instanceof TypeError) {
// Netzwerkfehler (kein Internet, CORS, etc.)
console.error('Netzwerkfehler:', error.message);
} else {
console.error(`Fetch fehlgeschlagen für ${url}:`, error.message);
}
return null;
}
}
Pattern 2: Error-First Tuple (Go-Style)
async function safeAsync(promise) {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error, null];
}
}
// Verwendung – kein try/catch nötig
const [error, user] = await safeAsync(getUser(1));
if (error) {
console.error('Konnte User nicht laden:', error);
return;
}
console.log('User:', user);
Pattern 3: Retry-Logik
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
console.warn(`Versuch ${attempt}/${maxRetries} fehlgeschlagen`);
if (attempt === maxRetries) throw error;
// Exponential Backoff: 1s, 2s, 4s...
await delay(Math.pow(2, attempt - 1) * 1000);
}
}
}
Praktisches Beispiel: API-Client
class ApiClient {
#baseUrl;
#defaultHeaders;
constructor(baseUrl, options = {}) {
this.#baseUrl = baseUrl;
this.#defaultHeaders = {
'Content-Type': 'application/json',
...options.headers
};
}
async #request(endpoint, options = {}) {
const url = `${this.#baseUrl}${endpoint}`;
const response = await fetch(url, {
headers: { ...this.#defaultHeaders, ...options.headers },
...options
});
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new ApiError(response.status, response.statusText, body);
}
if (response.status === 204) return null;
return response.json();
}
get(endpoint) { return this.#request(endpoint); }
post(endpoint, data) {
return this.#request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
put(endpoint, data) {
return this.#request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
delete(endpoint) {
return this.#request(endpoint, { method: 'DELETE' });
}
}
class ApiError extends Error {
constructor(status, statusText, body) {
super(`API Error ${status}: ${statusText}`);
this.status = status;
this.body = body;
}
}
// Verwendung
const api = new ApiClient('https://api.example.com');
try {
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'Max' });
await api.delete(`/users/${newUser.id}`);
} catch (error) {
if (error instanceof ApiError && error.status === 404) {
console.log('Ressource nicht gefunden');
} else {
throw error;
}
}
Häufige Fehler vermeiden
Vergessenes await
// FALSCH – gibt Promise zurück, nicht Daten
const data = fetchData();
console.log(data); // Promise { <pending> }
// RICHTIG
const data = await fetchData();
console.log(data); // Tatsächliche Daten
Sequenziell statt parallel
// LANGSAM – jeder Request wartet (~3s bei je 1s)
const users = await getUsers();
const posts = await getPosts();
const comments = await getComments();
// SCHNELL – alle starten sofort (~1s gesamt)
const [users, posts, comments] = await Promise.all([
getUsers(), getPosts(), getComments()
]);
await in Schleifen
// LANGSAM – sequenziell
for (const id of userIds) {
const user = await getUser(id);
users.push(user);
}
// SCHNELL – parallel
const users = await Promise.all(
userIds.map(id => getUser(id))
);
Unbehandelter Promise-Rejection
// GEFÄHRLICH – Fehler wird verschluckt
fetchData().then(data => console.log(data));
// SICHER
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));
// Global abfangen (Sicherheitsnetz)
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled rejection:', event.reason);
});
Microtasks und der Event Loop
Promises nutzen die Microtask Queue, die Vorrang vor der normalen Task Queue hat:
console.log('1: Synchron');
setTimeout(() => console.log('2: setTimeout (Task Queue)'), 0);
Promise.resolve().then(() => console.log('3: Promise (Microtask Queue)'));
console.log('4: Synchron');
// Ausgabe:
// 1: Synchron
// 4: Synchron
// 3: Promise (Microtask Queue) ← VOR setTimeout!
// 2: setTimeout (Task Queue)
Das zu verstehen ist wichtig, weil es erklärt warum Promise.resolve().then(...) immer vor setTimeout(..., 0) ausgeführt wird.
Fazit
Promises und async/await sind fundamentale Konzepte in modernem JavaScript:
async/awaitfür lesbaren, sequenziellen asynchronen CodePromise.all()für parallele unabhängige OperationenPromise.allSettled()wenn Fehler nicht alles stoppen sollenPromise.race()für Timeouts und Race-Conditions- Immer Error Handling implementieren – lokal und global
- In Schleifen
.map()+Promise.all()statt sequenziellesawait