Hvis du har skrevet JavaScript i mer enn noen måneder, har du kjent smerten av dypt nestede callbacks og floker av .then()-kjeder. async/await, introdusert i ES2017, løste alt dette — og likevel faller utviklere fortsatt i de samme tre eller fire fellene med det. La oss gå gjennom alt ordentlig: hvordan det fungerer, hvordan man håndterer feil godt, og de parallelle eksekveringsmønstrene som faktisk betyr noe for ytelse.
Grunnleggende — async-funksjoner og await
En async-funksjon returnerer alltid et Promise. Inne i den pauser await kjøringen til det avventede Promise-et avgjøres. Det er hele den mentale modellen:
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økkelordet await kan bare brukes inne i en async-funksjon — eller på toppnivået av en ES-modul (mer om det senere). Å bruke det andre steder er en syntaksfeil.
Feilhåndtering — den riktige og den gale måten
Det er her de fleste veiledninger går galt. Instinktet er å pakke alt inn i try/catch og kalle det ferdig. Det fungerer, men tomme catch-blokker er en kodelukt som skjuler ekte feil:
// ❌ 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 foretrekker et mønster der async-funksjoner enten returnerer null ved feil eller kaster bevisste feil. Det jeg unngår er å fange en feil, logge den, og deretter returnere en verdi som får den som kaller å tro at forespørselen lyktes.
await to(promise) som returnerer [error, data]-tupler.Sekvensielt vs parallelt — ytelsesfellen
Dette er feilen jeg ser oftest i produksjonskode. Når du await:er hvert kall etter hverandre, kjører du dem sekvensielt — selv når de er fullstendig uavhengige av hverandre:
// ❌ 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 forespørsler i samme øyeblikk og venter på at alle fullfører. Hvis noen av dem avvises, avvises hele greien. Det er vanligvis det du vil for dashboard-lignende innlasting der all data er påkrevd.
Promise.allSettled — når delvis feil er OK
Noen ganger vil du sende flere forespørsler og bruke det som kommer tilbake vellykket, selv om noen feiler. Promise.allSettled() er bygget for akkurat 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ønsteret er flott for ikke-kritiske UI-elementer — som en sidepanel med flere uavhengige seksjoner. Hvis én feiler, viser du resten i stedet for å tømme hele siden.
async i løkker — forEach-fellen
Denne har brent alle minst én gang. Array.forEach() avventer ikke async-callbacks — den sender dem og fortsetter umiddelbart. Løkken avsluttes før noe av det asynkrone arbeidet er ferdig:
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 processedBruk for...of når rekkefølgen betyr noe, eller når du trenger å begrense forespørsler (behandle én om gangen). Bruk Promise.all(map(...)) når du vil ha maksimal parallellitet og ikke trenger sekvensielle garantier.
await på toppnivå i ES-moduler
Siden ES2022 kan du bruke await på toppnivået av en ES-modul — ingen innpakningsfunksjon nødvendig. Det er en stor fordel for modulinitialisering som avhenger av asynkrone 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å toppnivå fungerer i Node.js 14.8+ med "type": "module" i package.json, og i alle moderne nettlesere via native ES-moduler. Den importerende modulens kjøring pauses inntil den avventede modulen er fullstendig løst — det er akkurat den garantien du trenger.
En ekte pipeline — hent, parse og transformer
Her er en realistisk async-pipeline som kombinerer alt: henting fra et API, håndtering av HTTP-feil, transformering av data og elegant fallback ved feil:
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 verktøy
Når du debugger async-kode som behandler JSON-data, hjelper disse verktøyene: JSON Formatter for å inspisere API-svar, JSON Validator for å fange misformede data før de når koden din, og JS Formatter for å rydde opp i async-funksjonskode. For hele async/await-spesifikasjonen er MDN async-guiden den mest grundige referansen, og TC39-spesifikasjonen dekker den nøyaktige semantikken hvis du trenger det.
Oppsummering
async/await gjør asynkron JavaScript lesbar — men fellene er reelle. Sjekk alltid response.ok før du parser, la aldri catch-blokker svelge feil stille, bruk Promise.all() for uavhengige parallelle kall, og hold deg unna forEach med async-callbacks. Lås inn disse vanene, og din async-kode vil være både rask og feilsøkbar.