Als je al een paar maanden JavaScript schrijft, heb je de pijn gevoeld van diep geneste callbacks en verwarde .then()-ketens. async/await, geïntroduceerd in ES2017, heeft dat allemaal opgelost — en toch lopen ontwikkelaars nog steeds tegen dezelfde drie of vier valkuilen aan. Laten we alles goed doorlopen: hoe het werkt, hoe je fouten goed afhandelt, en de parallelle uitvoeringspatronen die er echt toe doen voor prestaties.

De basis — async-functies en await

Een async-functie geeft altijd een Promise terug. Daarbinnen pauzeert await de uitvoering totdat de afgewachte Promise is afgewikkeld. Dat is het volledige mentale model:

js
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);

Het sleutelwoord await kan alleen worden gebruikt binnen een async-functie — of op het hoogste niveau van een ES-module (meer daarover later). Het ergens anders gebruiken is een syntaxisfout.

Foutafhandeling — De juiste en verkeerde manier

Dit is waar de meeste tutorials de mist in gaan. De neiging is om alles in try/catch te wikkelen en klaar. Dat werkt, maar lege catch-blokken zijn een codegeur die echte bugs maskeert:

js
// ❌ 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
  }
}

Ik geef de voorkeur aan een patroon waarbij async-functies null teruggeven bij een fout of opzettelijke fouten gooien. Wat ik vermijd is een fout opvangen, deze loggen en dan een waarde teruggeven waardoor de aanroeper denkt dat het verzoek is geslaagd.

Pro-tip: Als je meerdere awaits in één try-blok hebt, handelt één catch ze allemaal af — maar je verliest de context over welke er is mislukt. Overweeg voor complexe stromen aparte try/catch-blokken of een helper zoals await to(promise) die [error, data]-tuples teruggeeft.

Sequentieel vs Parallel — De prestatievalstrik

Dit is de fout die ik het vaakst zie in productiecode. Wanneer je elke aanroep één voor één awaitt, voer je ze sequentieel uit — zelfs als ze volledig onafhankelijk van elkaar zijn:

js
// ❌ 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() start alle drie verzoeken tegelijkertijd en wacht tot ze allemaal voltooid zijn. Als één ervan wordt afgewezen, wordt het geheel afgewezen. Dat is meestal wat je wilt voor dashboardstijl laden waarbij alle gegevens vereist zijn.

Promise.allSettled — Wanneer gedeeltelijk falen oké is

Soms wil je meerdere verzoeken starten en gebruiken wat succesvol terugkomt, zelfs als sommige mislukken. Promise.allSettled() is hier precies voor gebouwd:

js
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
}

Dit patroon is geweldig voor niet-kritieke UI-elementen — zoals een zijbalk met meerdere onafhankelijke secties. Als één mislukt, toon je de rest in plaats van de hele pagina leeg te laten.

async in lussen — Het forEach-probleem

Dit heeft iedereen minstens één keer gebrand. Array.forEach() wacht async-callbacks niet af — het start ze en gaat meteen verder. De lus is klaar voordat het async-werk gedaan is:

js
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 processed

Gebruik for...of wanneer volgorde belangrijk is of wanneer je verzoeken wilt beperken (één tegelijk verwerken). Gebruik Promise.all(map(...)) wanneer je maximale parallelisme wilt en geen sequentiële garanties nodig hebt.

Top-level await in ES-modules

Sinds ES2022 kun je await gebruiken op het hoogste niveau van een ES-module — geen wrapper-functie nodig. Dit is belangrijk voor module-initialisatie die afhankelijk is van async-gegevens:

js
// 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;
js
// 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 loaded

Top-level await werkt in Node.js 14.8+ met "type": "module" in package.json, en in alle moderne browsers via native ES-modules. De uitvoering van de importerende module wordt gepauzeerd totdat de afgewachte module volledig is opgelost — wat precies de garantie is die je nodig hebt.

Een echte pipeline — Ophalen, verwerken en transformeren

Hier is een realistische async-pipeline die alles combineert: ophalen van een API, HTTP-fouten afhandelen, gegevens transformeren en elegant terugvallen bij een fout:

js
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}`);
}

Handige hulpmiddelen

Wanneer je async-code debugt die JSON-payloads verwerkt, helpen deze tools: JSON Formatter om API-reacties te inspecteren, JSON Validator om misvormde payloads te detecteren voordat ze je code bereiken, en JS Formatter om async-functiecode op te schonen. Voor de volledige async/await-specificatie is de MDN async-gids de meest uitgebreide referentie, en de TC39-specificatie dekt de exacte semantiek als je die nodig hebt.

Samenvatting

async/await maakt asynchrone JavaScript leesbaar — maar de valkuilen zijn reëel. Controleer altijd response.ok voor het verwerken, laat catch-blokken fouten nooit stil opslokken, gebruik Promise.all() voor onafhankelijke parallelle aanroepen, en blijf weg van forEach met async-callbacks. Vergrendel die gewoonten en je async-code zal zowel snel als debuggable zijn.