Wenn Sie JavaScript seit mehr als ein paar Monaten schreiben, haben Sie den Schmerz tief verschachtelter Callbacks und verworrener .then()-Ketten gespürt. async/await, eingeführt in ES2017, hat das alles behoben — und dennoch laufen Entwickler immer wieder in dieselben drei oder vier Fallen. Lassen Sie uns das Ganze ordentlich durchgehen: wie es funktioniert, wie man Fehler richtig behandelt und die parallelen Ausführungsmuster, die wirklich für die Performance wichtig sind.
Die Grundlagen — async-Funktionen und await
Eine async-Funktion gibt immer ein Promise zurück. Darin hält await die Ausführung an, bis das erwartete Promise aufgelöst wird. Das ist das vollständige mentale Modell:
async function fetchUserProfile(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
const profile = await response.json();
return profile; // wrapped in a Promise automatically
}
// Calling an async function gives you a Promise
const profilePromise = fetchUserProfile(42);
profilePromise.then(profile => console.log(profile.name));
// Or use await at the call site
const profile = await fetchUserProfile(42);
console.log(profile.name);Das Schlüsselwort await darf nur innerhalb einer async-Funktion verwendet werden — oder auf der obersten Ebene eines ES-Moduls (dazu später mehr). Es anderswo zu verwenden, ist ein Syntaxfehler.
Fehlerbehandlung — Der richtige und der falsche Weg
Hier gehen die meisten Tutorials in die Irre. Der Instinkt ist, alles in try/catch zu verpacken und es als erledigt zu betrachten. Das funktioniert, aber leere catch-Blöcke sind ein Code-Smell, der echte Bugs verdeckt:
// ❌ Don't do this — silent failure, impossible to debug
async function loadConfig() {
try {
const res = await fetch('/api/config');
return await res.json();
} catch (err) {
// swallowed — you'll never know what broke
}
}
// ✅ Do this — handle errors explicitly, return something meaningful
async function loadConfig() {
try {
const res = await fetch('/api/config');
if (!res.ok) {
throw new Error(`Config fetch failed: ${res.status} ${res.statusText}`);
}
return await res.json();
} catch (err) {
console.error('loadConfig error:', err.message);
return null; // caller can check for null
}
}Ich bevorzuge ein Muster, bei dem async-Funktionen bei einem Fehler entweder null zurückgeben oder absichtliche Fehler werfen. Was ich vermeide, ist einen Fehler abzufangen, ihn zu loggen und dann einen Wert zurückzugeben, der dem Aufrufer das Gefühl gibt, die Anfrage sei erfolgreich gewesen.
await to(promise) in Betracht ziehen, der [error, data]-Tupel zurückgibt.Sequenziell vs. Parallel — Die Performance-Falle
Das ist der Fehler, den ich am häufigsten in Produktionscode sehe. Wenn Sie jede Anfrage nacheinander mit await aufrufen, führen Sie sie sequenziell aus — selbst wenn sie vollständig unabhängig voneinander sind:
// ❌ Sequential — takes ~900ms total (300 + 300 + 300)
async function loadDashboard(userId) {
const user = await fetchUser(userId); // 300ms
const orders = await fetchOrders(userId); // 300ms
const settings = await fetchSettings(userId); // 300ms
return { user, orders, settings };
}
// ✅ Parallel with Promise.all — takes ~300ms total
async function loadDashboard(userId) {
const [user, orders, settings] = await Promise.all([
fetchUser(userId),
fetchOrders(userId),
fetchSettings(userId)
]);
return { user, orders, settings };
}Promise.all() feuert alle drei Anfragen gleichzeitig ab und wartet, bis alle abgeschlossen sind. Wenn eine davon abgelehnt wird, wird das Ganze abgelehnt. Das ist normalerweise das, was Sie beim Dashboard-artigen Laden wollen, wo alle Daten erforderlich sind.
Promise.allSettled — Wenn Teilfehler in Ordnung sind
Manchmal möchten Sie mehrere Anfragen abfeuern und das verwenden, was erfolgreich zurückkommt, selbst wenn einige fehlschlagen. Promise.allSettled() wurde genau dafür entwickelt:
async function loadWidgets(widgetIds) {
const results = await Promise.allSettled(
widgetIds.map(id => fetchWidget(id))
);
const widgets = [];
const errors = [];
for (const result of results) {
if (result.status === 'fulfilled') {
widgets.push(result.value);
} else {
errors.push(result.reason.message);
}
}
if (errors.length > 0) {
console.warn('Some widgets failed to load:', errors);
}
return widgets; // return whatever succeeded
}Dieses Muster ist hervorragend für nicht-kritische UI-Elemente — wie eine Seitenleiste mit mehreren unabhängigen Abschnitten. Wenn einer fehlschlägt, zeigen Sie die anderen an, anstatt die gesamte Seite leer zu lassen.
async in Schleifen — Die forEach-Falle
Das hat jeden schon mindestens einmal erwischt. Array.forEach() wartet nicht auf async-Callbacks — es feuert sie ab und macht sofort weiter. Die Schleife endet, bevor asynchrone Arbeit abgeschlossen ist:
const orderIds = [101, 102, 103, 104];
// ❌ forEach ignores async — all requests fire in parallel uncontrolled,
// and code after the forEach runs before any complete
orderIds.forEach(async (id) => {
await processOrder(id); // NOT awaited by forEach
});
console.log('done?'); // prints before any order is processed
// ✅ for...of — sequential, fully awaited
for (const id of orderIds) {
await processOrder(id);
}
console.log('done'); // prints after all orders are processed
// ✅ Parallel but controlled — all fire at once, await all completions
await Promise.all(orderIds.map(id => processOrder(id)));
console.log('done'); // prints after all orders are processedVerwenden Sie for...of, wenn die Reihenfolge wichtig ist oder wenn Sie Anfragen drosseln müssen (eine nach der anderen verarbeiten). Verwenden Sie Promise.all(map(...)), wenn Sie maximale Parallelität wollen und keine sequenziellen Garantien benötigen.
Top-Level await in ES-Modulen
Seit ES2022 können Sie await auf der obersten Ebene eines ES-Moduls verwenden — keine Wrapper-Funktion erforderlich. Das ist großartig für die Modulinitialisierung, die von async-Daten abhängt:
// config.js (ES module)
const response = await fetch('/api/runtime-config');
const config = await response.json();
export const API_BASE_URL = config.apiBaseUrl;
export const FEATURE_FLAGS = config.featureFlags;// main.js — imports wait for config.js to fully resolve
import { API_BASE_URL, FEATURE_FLAGS } from './config.js';
console.log(API_BASE_URL); // guaranteed to be loadedTop-Level await funktioniert in Node.js 14.8+ mit "type": "module" in package.json und in allen modernen Browsern über native ES-Module. Die Ausführung des importierenden Moduls wird angehalten, bis das erwartete Modul vollständig aufgelöst ist — genau die Garantie, die Sie benötigen.
Eine echte Pipeline — Abrufen, Parsen und Transformieren
Hier ist eine realistische async-Pipeline, die alles kombiniert: Abrufen von einer API, Behandlung von HTTP-Fehlern, Transformation der Daten und graceful Fallback bei Fehler:
async function getProductCatalog(categoryId) {
// Step 1: fetch raw data
const response = await fetch(
`https://api.shop.example.com/categories/${categoryId}/products`,
{ headers: { Authorization: `Bearer ${getAuthToken()}` } }
);
if (!response.ok) {
throw new Error(`Catalog fetch failed: ${response.status}`);
}
// Step 2: parse JSON
const raw = await response.json();
// Step 3: transform into the shape your UI needs
const products = raw.items.map(item => ({
id: item.product_id,
name: item.display_name,
price: (item.price_cents / 100).toFixed(2),
inStock: item.inventory_count > 0,
imageUrl: item.media?.[0]?.url ?? '/images/placeholder.png'
}));
// Step 4: filter out anything that's been discontinued
return products.filter(p => !p.discontinued);
}
// Usage
try {
const catalog = await getProductCatalog('electronics');
renderProductGrid(catalog);
} catch (err) {
showErrorBanner(`Could not load products: ${err.message}`);
}Nützliche Werkzeuge
Wenn Sie async-Code debuggen, der JSON-Payloads verarbeitet, helfen diese Werkzeuge: JSON-Formatierer zum Überprüfen von API-Antworten, JSON-Validator zum Erkennen fehlerhafter Payloads, bevor sie Ihren Code erreichen, und JS-Formatierer zum Aufräumen von async-Funktionscode. Für die vollständige async/await-Spezifikation ist der MDN async-Leitfaden die umfassendste Referenz, und die TC39-Spezifikation behandelt die genaue Semantik, wenn Sie diese benötigen.
Fazit
async/await macht asynchrones JavaScript lesbar — aber die Fallen sind real. Prüfen Sie immer response.ok vor dem Parsen, lassen Sie catch-Blöcke Fehler niemals still schlucken, verwenden Sie Promise.all() für unabhängige parallele Aufrufe und meiden Sie forEach mit async-Callbacks. Wenn Sie diese Gewohnheiten verinnerlicht haben, wird Ihr async-Code sowohl schnell als auch debuggbar sein.