JavaScript Promises und Async/Await - Vollständiger Leitfaden
Asynchrone Programmierung ist das Fundament der modernen JavaScript-Entwicklung. Dieses umfassende Tutorial behandelt Promises, async/await und fortgeschrittene asynchrone Muster, die Sie zu einem effektiveren JavaScript-Entwickler machen werden.
Asynchrones JavaScript verstehen
Was ist asynchrone Programmierung?
Asynchrone Programmierung ermöglicht es Code, ohne Blockierung des Hauptthreads zu laufen, wodurch JavaScript mehrere Operationen gleichzeitig handhaben kann. Dies ist entscheidend für:
- Netzwerkanfragen (API-Aufrufe)
- Dateioperationen (Lesen/Schreiben von Dateien)
- Datenbankabfragen (Verbindung zu Datenbanken)
- Timer-Operationen (setTimeout, setInterval)
- Benutzerinteraktionen (Event-Handling)
Die Event Loop
JavaScript läuft auf einem einzigen Thread, verwendet aber eine Event Loop, um asynchrone Operationen zu handhaben:
javascript
// Synchroner Code - blockiert die Ausführung
console.log('Start');
console.log('Middle');
console.log('End');
// Asynchroner Code - nicht blockierend
console.log('Start');
setTimeout(() => {
console.log('Async operation');
}, 0);
console.log('End');
// Ausgabe:
// Start
// End
// Async operation
Callback-Muster (Legacy)
Vor Promises verwendete JavaScript Callbacks für asynchrone Operationen:
javascript
// Callback-Beispiel
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: 'John' };
callback(null, data);
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error('Error:', error);
} else {
console.log('Data:', data);
}
});
Callback Hell Problem
javascript
// Verschachtelte Callbacks - schwer zu lesen und zu warten
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
// Das ist Callback Hell!
console.log('Final result:', d);
});
});
});
});
Promises verstehen
Was ist ein Promise?
Ein Promise ist ein Objekt, das die eventuelle Vervollständigung oder das Scheitern einer asynchronen Operation repräsentiert. Es hat drei Zustände:
- Pending - Anfangszustand, weder erfüllt noch abgelehnt
- Fulfilled - Operation erfolgreich abgeschlossen
- Rejected - Operation fehlgeschlagen
Promises erstellen
Grundlegende Promise-Syntax
javascript
// Promise erstellen
const myPromise = new Promise((resolve, reject) => {
// Asynchrone Operation
const success = true;
if (success) {
resolve('Operation erfolgreich!');
} else {
reject('Operation fehlgeschlagen!');
}
});
// Promise verwenden
myPromise
.then(result => {
console.log('Erfolg:', result);
})
.catch(error => {
console.log('Fehler:', error);
});
Promise mit Timeout
javascript
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
delay(2000).then(() => {
console.log('2 Sekunden sind vergangen');
});
Promise mit Daten
javascript
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({
id: id,
name: `User ${id}`,
email: `user${id}@example.com`
});
} else {
reject(new Error('Ungültige Benutzer-ID'));
}
}, 1000);
});
}
fetchUser(1)
.then(user => {
console.log('User:', user);
})
.catch(error => {
console.error('Fehler:', error.message);
});
Promise-Methoden
.then()
Methode
javascript
fetchUser(1)
.then(user => {
console.log('Benutzer erhalten:', user.name);
return user.id; // Daten an nächstes .then() weiterleiten
})
.then(userId => {
console.log('Benutzer-ID:', userId);
return fetchUserPosts(userId);
})
.then(posts => {
console.log('Benutzer-Posts:', posts);
});
.catch()
Methode
javascript
fetchUser(-1)
.then(user => {
console.log('User:', user);
})
.catch(error => {
console.error('Fehler abgefangen:', error.message);
});
.finally()
Methode
javascript
fetchUser(1)
.then(user => {
console.log('Erfolg:', user);
})
.catch(error => {
console.error('Fehler:', error);
})
.finally(() => {
console.log('Operation abgeschlossen');
// Aufräumcode hier
});
Promise-Verkettung
javascript
function fetchUserData(userId) {
return fetchUser(userId)
.then(user => {
console.log('Benutzer abgerufen:', user.name);
return fetchUserPosts(user.id);
})
.then(posts => {
console.log('Posts abgerufen:', posts.length);
return fetchUserComments(posts[0].id);
})
.then(comments => {
console.log('Kommentare abgerufen:', comments.length);
return comments;
})
.catch(error => {
console.error('Fehler in der Kette:', error);
throw error; // Für upstream-Behandlung erneut werfen
});
}
Statische Promise-Methoden
Promise.all()
- Parallele Ausführung
javascript
// Warten bis alle Promises aufgelöst sind
const promises = [
fetchUser(1),
fetchUser(2),
fetchUser(3)
];
Promise.all(promises)
.then(users => {
console.log('Alle Benutzer:', users);
// Alle Promises aufgelöst
})
.catch(error => {
console.error('Ein oder mehrere Promises fehlgeschlagen:', error);
// Wenn IRGENDEINES Promise fehlschlägt, läuft dieser catch
});
Promise.allSettled()
- Alle Ergebnisse
javascript
// Warten bis alle Promises abgeschlossen sind (aufgelöst oder abgelehnt)
const promises = [
fetchUser(1),
fetchUser(-1), // Dies wird abgelehnt
fetchUser(3)
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} aufgelöst:`, result.value);
} else {
console.log(`Promise ${index} abgelehnt:`, result.reason);
}
});
});
Promise.race()
- Erstes zu beenden
javascript
// Auflösung mit dem ersten Promise, das abgeschlossen wird
const promises = [
delay(1000).then(() => 'Erstes'),
delay(2000).then(() => 'Zweites'),
delay(500).then(() => 'Drittes')
];
Promise.race(promises)
.then(result => {
console.log('Gewinner:', result); // "Drittes"
});
Promise.any()
- Erstes zu erfüllen
javascript
// Auflösung mit dem ersten Promise, das erfüllt wird
const promises = [
Promise.reject('Fehler 1'),
delay(1000).then(() => 'Erfolg 1'),
delay(500).then(() => 'Erfolg 2')
];
Promise.any(promises)
.then(result => {
console.log('Erster Erfolg:', result); // "Erfolg 2"
})
.catch(error => {
console.log('Alle Promises abgelehnt:', error);
});
Async/Await Syntax
Was ist Async/Await?
Async/await ist syntaktischer Zucker, der auf Promises aufbaut und asynchronen Code aussehen und verhalten lässt wie synchronen Code.
Grundlegendes Async/Await
javascript
// Promise-basierter Ansatz
function fetchUserPromise(id) {
return fetchUser(id)
.then(user => {
console.log('User:', user);
return fetchUserPosts(user.id);
})
.then(posts => {
console.log('Posts:', posts);
return posts;
});
}
// Async/await Ansatz
async function fetchUserAsync(id) {
try {
const user = await fetchUser(id);
console.log('User:', user);
const posts = await fetchUserPosts(user.id);
console.log('Posts:', posts);
return posts;
} catch (error) {
console.error('Fehler:', error);
throw error;
}
}
Async-Funktionsdeklaration
javascript
// Async-Funktionsdeklaration
async function getData() {
return 'data';
}
// Async-Funktionsausdruck
const getData = async function() {
return 'data';
};
// Async-Arrow-Funktion
const getData = async () => {
return 'data';
};
// Async-Methode in Objekt
const obj = {
async getData() {
return 'data';
}
};
// Async-Methode in Klasse
class DataService {
async getData() {
return 'data';
}
}
Await-Schlüsselwort
javascript
async function processData() {
// await kann nur innerhalb von async-Funktionen verwendet werden
const data = await fetchData();
const processedData = await processDataStep(data);
const result = await saveData(processedData);
return result;
}
// Dies würde einen Fehler verursachen - await außerhalb einer async-Funktion
// const data = await fetchData(); // SyntaxError
Fehlerbehandlung mit Try/Catch
javascript
async function handleErrors() {
try {
const user = await fetchUser(1);
const posts = await fetchUserPosts(user.id);
const comments = await fetchUserComments(posts[0].id);
return { user, posts, comments };
} catch (error) {
console.error('Fehler aufgetreten:', error);
// Spezifische Fehlertypen behandeln
if (error.message.includes('network')) {
throw new Error('Netzwerkfehler aufgetreten');
} else if (error.message.includes('auth')) {
throw new Error('Authentifizierung fehlgeschlagen');
} else {
throw new Error('Unbekannter Fehler aufgetreten');
}
}
}
Praktische Beispiele
1. API-Datenabruf
javascript
// Fetch API mit async/await
async function fetchUserProfile(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP-Fehler! Status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('Benutzer konnte nicht abgerufen werden:', error);
throw error;
}
}
// Verwendung
async function displayUserProfile(userId) {
try {
const user = await fetchUserProfile(userId);
document.getElementById('username').textContent = user.name;
document.getElementById('email').textContent = user.email;
} catch (error) {
document.getElementById('error').textContent = 'Benutzerprofil konnte nicht geladen werden';
}
}
2. Sequenzielle vs. parallele Operationen
javascript
// Sequenzielle Operationen (langsamer)
async function sequentialOperations() {
const startTime = Date.now();
const user = await fetchUser(1); // 1 Sekunde warten
const posts = await fetchUserPosts(1); // Weitere 1 Sekunde warten
const comments = await fetchComments(1); // Weitere 1 Sekunde warten
const endTime = Date.now();
console.log(`Sequenziell dauerte ${endTime - startTime}ms`); // ~3000ms
return { user, posts, comments };
}
// Parallele Operationen (schneller)
async function parallelOperations() {
const startTime = Date.now();
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchUserPosts(1),
fetchComments(1)
]);
const endTime = Date.now();
console.log(`Parallel dauerte ${endTime - startTime}ms`); // ~1000ms
return { user, posts, comments };
}
3. Mehrere asynchrone Operationen handhaben
javascript
async function loadDashboardData(userId) {
try {
// Alle Operationen parallel starten
const userPromise = fetchUser(userId);
const postsPromise = fetchUserPosts(userId);
const notificationsPromise = fetchNotifications(userId);
const settingsPromise = fetchUserSettings(userId);
// Zuerst auf kritische Daten warten
const user = await userPromise;
// Auf den Rest warten
const [posts, notifications, settings] = await Promise.all([
postsPromise,
notificationsPromise,
settingsPromise
]);
return {
user,
posts,
notifications,
settings
};
} catch (error) {
console.error('Dashboard-Laden fehlgeschlagen:', error);
throw error;
}
}
4. Retry-Logik mit Async/Await
javascript
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.log(`Versuch ${i + 1} fehlgeschlagen:`, error.message);
if (i === maxRetries - 1) {
throw error; // Letzter Versuch fehlgeschlagen
}
// Vor Wiederholung warten
await delay(1000 * (i + 1)); // Exponentieller Backoff
}
}
}
5. Timeout-Implementierung
javascript
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Operation timeout')), ms);
});
}
async function fetchWithTimeout(url, timeoutMs = 5000) {
try {
const result = await Promise.race([
fetch(url),
timeout(timeoutMs)
]);
return await result.json();
} catch (error) {
if (error.message === 'Operation timeout') {
throw new Error('Anfrage timeout');
}
throw error;
}
}
Erweiterte Muster
1. Promise-Warteschlange
javascript
class PromiseQueue {
constructor() {
this.queue = [];
this.running = false;
}
async add(promiseFunction) {
return new Promise((resolve, reject) => {
this.queue.push({
promiseFunction,
resolve,
reject
});
this.run();
});
}
async run() {
if (this.running) return;
this.running = true;
while (this.queue.length > 0) {
const { promiseFunction, resolve, reject } = this.queue.shift();
try {
const result = await promiseFunction();
resolve(result);
} catch (error) {
reject(error);
}
}
this.running = false;
}
}
// Verwendung
const queue = new PromiseQueue();
queue.add(() => fetchUser(1)).then(user => console.log('User 1:', user));
queue.add(() => fetchUser(2)).then(user => console.log('User 2:', user));
queue.add(() => fetchUser(3)).then(user => console.log('User 3:', user));
2. Async-Iterator
javascript
async function* fetchUsersGenerator() {
let page = 1;
while (true) {
const response = await fetch(`/api/users?page=${page}`);
const data = await response.json();
if (data.users.length === 0) break;
yield data.users;
page++;
}
}
// Verwendung
async function processAllUsers() {
for await (const users of fetchUsersGenerator()) {
console.log(`${users.length} Benutzer verarbeiten`);
// Benutzer verarbeiten
}
}
3. Gedrosselte Async-Funktion
javascript
function debounceAsync(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
return new Promise((resolve, reject) => {
timeoutId = setTimeout(async () => {
try {
const result = await func.apply(this, args);
resolve(result);
} catch (error) {
reject(error);
}
}, delay);
});
};
}
// Verwendung
const debouncedSearch = debounceAsync(async (query) => {
const response = await fetch(`/api/search?q=${query}`);
return await response.json();
}, 300);
// Nur die letzte Suche innerhalb von 300ms wird ausgeführt
debouncedSearch('javascript');
debouncedSearch('javascrip');
debouncedSearch('javascript promises'); // Diese wird ausgeführt
4. Async Map mit Nebenläufigkeitsbegrenzung
javascript
async function mapWithConcurrency(array, asyncFn, concurrency = 5) {
const results = [];
for (let i = 0; i < array.length; i += concurrency) {
const batch = array.slice(i, i + concurrency);
const batchPromises = batch.map(asyncFn);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
}
return results;
}
// Verwendung
const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const users = await mapWithConcurrency(userIds, fetchUser, 3);
Best Practices für Fehlerbehandlung
1. Angemessene Fehlerweiterleitung
javascript
async function processUserData(userId) {
try {
const user = await fetchUser(userId);
const processedData = await processData(user);
return processedData;
} catch (error) {
// Fehler protokollieren
console.error('Fehler in processUserData:', error);
// Fehler bei Bedarf transformieren
if (error.message.includes('not found')) {
throw new Error(`Benutzer ${userId} nicht gefunden`);
}
// Ursprünglichen Fehler erneut werfen
throw error;
}
}
2. Graceful Degradation
javascript
async function loadUserDashboard(userId) {
const results = await Promise.allSettled([
fetchUser(userId),
fetchUserPosts(userId),
fetchNotifications(userId),
fetchUserSettings(userId)
]);
const [userResult, postsResult, notificationsResult, settingsResult] = results;
return {
user: userResult.status === 'fulfilled' ? userResult.value : null,
posts: postsResult.status === 'fulfilled' ? postsResult.value : [],
notifications: notificationsResult.status === 'fulfilled' ? notificationsResult.value : [],
settings: settingsResult.status === 'fulfilled' ? settingsResult.value : getDefaultSettings()
};
}
3. Error Boundaries mit Async/Await
javascript
class AsyncErrorBoundary {
constructor() {
this.errorHandlers = new Map();
}
addErrorHandler(errorType, handler) {
this.errorHandlers.set(errorType, handler);
}
async execute(asyncFunction) {
try {
return await asyncFunction();
} catch (error) {
const handler = this.errorHandlers.get(error.constructor.name);
if (handler) {
return await handler(error);
}
throw error;
}
}
}
// Verwendung
const errorBoundary = new AsyncErrorBoundary();
errorBoundary.addErrorHandler('NetworkError', async (error) => {
console.log('Netzwerkfehler behandelt');
return getCachedData();
});
errorBoundary.addErrorHandler('AuthError', async (error) => {
console.log('Auth-Fehler behandelt');
await refreshToken();
return retryOperation();
});
Async Code testen
1. Promises testen
javascript
// Jest-Beispiel
describe('User API', () => {
test('sollte Benutzer erfolgreich abrufen', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('id', 1);
expect(user).toHaveProperty('name');
});
test('sollte bei ungültiger ID ablehnen', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Ungültige Benutzer-ID');
});
test('sollte Netzwerkfehler behandeln', async () => {
// Fetch mocken um Netzwerkfehler zu simulieren
global.fetch = jest.fn().mockRejectedValue(new Error('Netzwerkfehler'));
await expect(fetchUserProfile(1)).rejects.toThrow('Netzwerkfehler');
});
});
2. Testen mit Mocks
javascript
// Async-Funktionen mocken
const mockFetchUser = jest.fn();
mockFetchUser.mockResolvedValue({ id: 1, name: 'John' });
test('sollte Benutzerdaten verarbeiten', async () => {
const result = await processUserData(1);
expect(mockFetchUser).toHaveBeenCalledWith(1);
expect(result).toEqual({ id: 1, name: 'John' });
});
3. Timeouts testen
javascript
test('sollte nach 5 Sekunden timeout', async () => {
const slowPromise = new Promise(resolve => {
setTimeout(resolve, 6000);
});
await expect(
Promise.race([
slowPromise,
timeout(5000)
])
).rejects.toThrow('Operation timeout');
});
Performance-Überlegungen
1. Blockierende Operationen vermeiden
javascript
// ❌ Schlecht: Blockierende Schleife
async function processItemsSequentially(items) {
const results = [];
for (const item of items) {
const result = await processItem(item); // Blockiert bei jedem Item
results.push(result);
}
return results;
}
// ✅ Gut: Parallele Verarbeitung
async function processItemsInParallel(items) {
const promises = items.map(item => processItem(item));
return await Promise.all(promises);
}
// ✅ Gut: Kontrollierte Nebenläufigkeit
async function processItemsWithConcurrency(items, concurrency = 5) {
const results = [];
for (let i = 0; i < items.length; i += concurrency) {
const batch = items.slice(i, i + concurrency);
const batchResults = await Promise.all(batch.map(processItem));
results.push(...batchResults);
}
return results;
}
2. Speicherverwaltung
javascript
// ❌ Schlecht: Speicherleck bei langanhaltenden Operationen
async function processLargeDataset(data) {
const results = [];
for (const item of data) {
const result = await processItem(item);
results.push(result); // Alle Ergebnisse im Speicher ansammeln
}
return results;
}
// ✅ Gut: Stream-Verarbeitung
async function* processLargeDatasetStream(data) {
for (const item of data) {
const result = await processItem(item);
yield result; // Einzeln verarbeiten
}
}
// Verwendung
for await (const result of processLargeDatasetStream(data)) {
console.log(result);
// Ergebnis sofort verarbeiten, nicht alle speichern
}
Häufige Fallen und Lösungen
1. Vergessenes Await
javascript
// ❌ Schlecht: Await vergessen
async function badExample() {
const user = fetchUser(1); // Gibt Promise zurück, nicht Benutzerdaten
console.log(user.name); // Fehler: Kann Eigenschaft 'name' von undefined nicht lesen
}
// ✅ Gut: Korrektes await verwenden
async function goodExample() {
const user = await fetchUser(1);
console.log(user.name);
}
2. Promises und Async/Await mischen
javascript
// ❌ Schlecht: Muster mischen
async function mixedExample() {
return fetchUser(1).then(user => {
return processUser(user);
});
}
// ✅ Gut: Konsistentes async/await
async function consistentExample() {
const user = await fetchUser(1);
return await processUser(user);
}
3. Nicht behandelte abgelehnte Promises
javascript
// ❌ Schlecht: Unbehandelte Promise-Ablehnung
async function unhandledExample() {
const promises = [
fetchUser(1),
fetchUser(2),
fetchUser(-1) // Dies wird abgelehnt
];
const results = await Promise.all(promises); // Wirft Fehler
}
// ✅ Gut: Angemessene Fehlerbehandlung
async function handledExample() {
const promises = [
fetchUser(1),
fetchUser(2),
fetchUser(-1)
];
const results = await Promise.allSettled(promises);
const successfulResults = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
const errors = results
.filter(result => result.status === 'rejected')
.map(result => result.reason);
return { successfulResults, errors };
}
Best Practices Zusammenfassung
1. Async/Await für Lesbarkeit verwenden
javascript
// ✅ Bevorzugt: Sauber und lesbar
async function getUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchUserPosts(userId);
return { user, posts };
} catch (error) {
throw new Error(`Benutzerdaten abrufen fehlgeschlagen: ${error.message}`);
}
}
// ❌ Vermeiden: Promise-Ketten wenn async/await sauberer ist
function getUserDataPromise(userId) {
return fetchUser(userId)
.then(user => {
return fetchUserPosts(userId)
.then(posts => ({ user, posts }));
})
.catch(error => {
throw new Error(`Benutzerdaten abrufen fehlgeschlagen: ${error.message}`);
});
}
2. Fehler angemessen behandeln
javascript
// ✅ Gut: Umfassende Fehlerbehandlung
async function robustFunction() {
try {
const result = await riskyOperation();
return result;
} catch (error) {
// Zum Debuggen protokollieren
console.error('Operation fehlgeschlagen:', error);
// Fehler für Benutzer transformieren
if (error.code === 'NETWORK_ERROR') {
throw new Error('Bitte überprüfen Sie Ihre Internetverbindung');
}
// Fallback bereitstellen
return getDefaultValue();
}
}
3. Parallele Verarbeitung wenn möglich verwenden
javascript
// ✅ Gut: Parallele unabhängige Operationen
async function efficientDataLoading() {
const [user, settings, notifications] = await Promise.all([
fetchUser(userId),
fetchUserSettings(userId),
fetchNotifications(userId)
]);
return { user, settings, notifications };
}
Fazit
JavaScript Promises und async/await sind mächtige Werkzeuge für den Umgang mit asynchronen Operationen. Wichtige Punkte:
- Async/await für saubereren Code verwenden - Lesbarer als Promise-Ketten
- Fehler angemessen behandeln - Immer try/catch mit async/await verwenden
- Promise-Methoden verstehen -
Promise.all()
,Promise.allSettled()
, etc. - Performance bedenken - Parallele Verarbeitung wenn angebracht verwenden
- Async-Code gründlich testen - Externe Abhängigkeiten mocken
- Häufige Fallen vermeiden - Await nicht vergessen, Ablehnungen behandeln
- Angemessene Muster verwenden - Das richtige Werkzeug für die Aufgabe wählen
Die Beherrschung von asynchronem JavaScript ist wesentlich für moderne Webentwicklung. Mit diesen Konzepten und Mustern können Sie effizienten, wartbaren asynchronen Code schreiben.
Nächste Schritte
Nach der Beherrschung von Promises und async/await erkunden Sie:
- Web APIs - Fetch API, Web Workers, Service Workers
- Reaktive Programmierung - RxJS und Observables
- Node.js Streams - Arbeiten mit Datenströmen
- GraphQL - Moderne API-Abfragesprache
- WebSockets - Echtzeitkommunikation
- Performance-Optimierung - Debouncing, Throttling, Caching
Asynchrone Programmierung ist das Herzstück von modernem JavaScript - beherrschen Sie diese Konzepte, um responsive, effiziente Anwendungen zu erstellen!