Om du har skrivit JavaScript i mer än några månader har du känt smärtan av djupt nästlade callbacks och snåriga .then()-kedjor. async/await, introducerat i ES2017, löste allt det — och ändå faller utvecklare fortfarande i samma tre eller fyra fällor med det. Låt oss gå igenom det ordentligt: hur det fungerar, hur man hanterar fel väl och de parallella exekveringsmönster som faktiskt spelar roll för prestanda.
Grunderna — async-funktioner och await
En async-funktion returnerar alltid ett Promise. Inuti den pausar await exekveringen tills det avvaktade Promise:t avgörs. Det är hela den mentala 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);Nyckelordet await kan bara användas inuti en async-funktion — eller på toppnivån av en ES-modul (mer om det senare). Att använda det någon annanstans är ett syntaxfel.
Felhantering — det rätta och det felaktiga sättet
Det är här de flesta guider går fel. Instinkten är att linda allt i try/catch och kalla det klart. Det fungerar, men tomma catch-block är en kodlukt som maskerar verkliga buggar:
// ❌ 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
}
}Jag föredrar ett mönster där async-funktioner antingen returnerar null vid fel eller kastar avsiktliga fel. Vad jag undviker är att fånga ett fel, logga det och sedan returnera ett värde som får anroparen att tro att begäran lyckades.
await to(promise) som returnerar [error, data]-tupler.Sekventiellt vs parallellt — prestandafällan
Det här är misstaget jag ser oftast i produktionskod. När du await:ar varje anrop efter varandra kör du dem sekventiellt — även när de är helt oberoende av varandra:
// ❌ 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() skickar alla tre begäranden i samma ögonblick och väntar på att alla ska slutföras. Om någon av dem avvisas avvisas hela saken. Det är vanligtvis vad du vill för dashboardladdning där all data krävs.
Promise.allSettled — när partiellt misslyckande är OK
Ibland vill du skicka flera begäranden och använda det som kommer tillbaka framgångsrikt, även om vissa misslyckas. Promise.allSettled() är byggd exakt för 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
}Det här mönstret är utmärkt för icke-kritiska UI-element — som ett sidofält med flera oberoende sektioner. Om en misslyckas visar du resten istället för att tömma hela sidan.
async i loopar — forEach-fällan
Det här har bränt alla minst en gång. Array.forEach() avvaktar inte async-callbacks — den skickar dem och fortsätter omedelbart. Loopen avslutas innan något av det asynkrona arbetet är klart:
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 processedAnvänd for...of när ordningen spelar roll eller när du behöver begränsa begäranden (bearbeta en i taget). Använd Promise.all(map(...)) när du vill ha maximal parallellism och inte behöver sekventiella garantier.
Toppnivå-await i ES-moduler
Sedan ES2022 kan du använda await på toppnivån av en ES-modul — ingen omslagsfunktion behövs. Det är stort för modulinitialisering som beror på asynkron 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 loadedToppnivå-await fungerar i Node.js 14.8+ med "type": "module" i package.json, och i alla moderna webbläsare via inbyggda ES-moduler. Den importerande modulens exekvering pausas tills den avvaktade modulen är fullständigt löst — vilket är precis den garanti du behöver.
En verklig pipeline — hämta, parsa och transformera
Här är en realistisk async-pipeline som kombinerar allt: hämtning från ett API, hantering av HTTP-fel, transformering av data och elegant återgång vid misslyckande:
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}`);
}Användbara verktyg
När du felsöker async-kod som bearbetar JSON-data hjälper dessa verktyg: JSON Formatter för att inspektera API-svar, JSON Validator för att fånga felaktiga data innan de når din kod, och JS Formatter för att städa upp async-funktionskod. För hela async/await-specifikationen är MDN:s async-guide den mest grundliga referensen, och TC39-specifikationen täcker den exakta semantiken om du behöver den.
Sammanfattning
async/await gör asynkron JavaScript läsbar — men fällorna är verkliga. Kontrollera alltid response.ok innan du parsar, låt aldrig catch-block svälja fel tyst, använd Promise.all() för oberoende parallella anrop och håll dig borta från forEach med async-callbacks. Lås in de vanorna och din async-kod kommer att vara både snabb och felsökningsbar.