Hvis du har skrevet JavaScript i mere end et par måneder, har du mærket smerten ved dybt indlejrede callbacks og sammenfiltrede .then()-kæder. async/await, introduceret i ES2017, løste alt det — og alligevel falder udviklere stadig i de samme tre eller fire fælder med det. Lad os gennemgå det hele ordentligt: hvordan det virker, hvordan man håndterer fejl godt og de parallelle eksekveringsmønstre, der faktisk betyder noget for ydeevnen.
Grundlæggende — async-funktioner og await
En async-funktion returnerer altid et Promise. Inde i den pauser await eksekveringen, indtil det afventede Promise afklares. Det er hele den mentale model:
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);Nøgleordet await kan kun bruges inde i en async-funktion — eller på topniveauet af et ES-modul (mere om det senere). At bruge det et andet sted er en syntaksfejl.
Fejlhåndtering — den rigtige og den forkerte måde
Det er her de fleste guides går galt. Instinktet er at pakke alt ind i try/catch og kalde det gjort. Det virker, men tomme catch-blokke er en kodelugt, der maskerer rigtige fejl:
// ❌ 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
}
}Jeg foretrækker et mønster, hvor async-funktioner enten returnerer null ved fejl eller kaster bevidste fejl. Det, jeg undgår, er at fange en fejl, logge den og derefter returnere en værdi, der får den, der kalder, til at tro, at anmodningen lykkedes.
await to(promise), der returnerer [error, data]-tupler.Sekventielt vs parallelt — ydelsesfælden
Det er den fejl, jeg oftest ser i produktionskode. Når du await:er hvert kald efter hinanden, kører du dem sekventielt — selv når de er fuldstændig uafhængige af hinanden:
// ❌ 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() sender alle tre anmodninger på samme øjeblik og venter på, at alle er færdige. Hvis nogen af dem afvises, afvises det hele. Det er normalt, hvad du vil have til dashboardlignende indlæsning, hvor alle data er nødvendige.
Promise.allSettled — når delvis fejl er OK
Nogle gange vil du sende flere anmodninger og bruge det, der kommer tilbage succesfuldt, selv hvis nogle fejler. Promise.allSettled() er bygget til præcis det:
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
}Dette mønster er fantastisk til ikke-kritiske UI-elementer — som en sidebjælke med flere uafhængige sektioner. Hvis én fejler, viser du resten i stedet for at tømme hele siden.
async i løkker — forEach-fælden
Denne har brændt alle mindst én gang. Array.forEach() afventer ikke async-callbacks — den sender dem og fortsætter straks. Løkken afsluttes, før noget af det asynkrone arbejde er udført:
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 processedBrug for...of, når rækkefølgen betyder noget, eller når du skal begrænse anmodninger (behandl én ad gangen). Brug Promise.all(map(...)), når du vil have maksimal parallelisme og ikke har brug for sekventielle garantier.
await på topniveau i ES-moduler
Siden ES2022 kan du bruge await på topniveauet af et ES-modul — ingen indpakningsfunktion nødvendig. Det er vigtigt for modulinitialisering, der afhænger af async-data:
// 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 loadedawait på topniveau fungerer i Node.js 14.8+ med "type": "module" i package.json og i alle moderne browsere via native ES-moduler. Det importerende moduls eksekvering pauses, indtil det afventede modul er fuldt løst — hvilket er præcis den garanti, du har brug for.
En ægte pipeline — hent, parse og transformer
Her er en realistisk async-pipeline, der kombinerer det hele: hentning fra et API, håndtering af HTTP-fejl, transformation af data og elegant fallback ved fejl:
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}`);
}Nyttige værktøjer
Når du debugger async-kode, der behandler JSON-data, hjælper disse værktøjer: JSON Formatter til at inspicere API-svar, JSON Validator til at fange misdannede data, før de rammer din kode, og JS Formatter til at rydde op i async-funktionskode. For den fulde async/await-specifikation er MDN async-guiden den mest grundige reference, og TC39-specifikationen dækker den præcise semantik, hvis du har brug for dem.
Afslutning
async/await gør asynkron JavaScript læsbar — men fælderne er reelle. Tjek altid response.ok, før du parser, lad aldrig catch-blokke sluge fejl stille, brug Promise.all() til uafhængige parallelle kald, og hold dig væk fra forEach med async-callbacks. Lås disse vaner ind, og din async-kode vil være både hurtig og fejlfindbar.