Async/await er ikke magi, det er bare langsomme bugs i forklædning

Async/await er ikke magi, det er bare langsomme bugs i forklædning

Din mentale model af async/await er nok forkert

Jeg troede oprigtigt, at jeg havde forstået async/await, indtil jeg en aften sad med 30+ UnhandledPromiseRejection-fejl i konsollen og ingen anelse om, hvor de kom fra.

Hvis du også har haft den der følelse af, at “jeg gør jo alt rigtigt” og alligevel får mærkelige bugs, så handler det tit om, at vores mentale model af async/await halter lidt.

Den korte version: await gør ikke din kode “synkron”. Det gør bare, at funktionen pauser, indtil et promise er afgjort (resolved eller rejected). Alt andet kører videre. Og alle promises, du ikke venter på, lever videre på egen hånd.

Lad os bygge videre derfra og se på de 7 fejl, jeg oftest ser (og laver). Hver fejl får:

  • Et symptom (det du ser i konsollen / i appen)
  • En forklaring (hvad der faktisk sker)
  • Et fix (hvordan du kan skrive det anderledes)

Fejl 1: Du glemmer await og tror fejlen er væk

Den her er så lille, at man næsten skammer sig over den. Næsten.

async function loadData() {
  try {
    fetch('/api/data'); // ingen await
    console.log('Data hentet');
  } catch (err) {
    console.error('Fejl i loadData', err);
  }
}

Symptom: Der står “Data hentet” i konsollen, men du har ingen data, og nogle gange dukker der en UnhandledPromiseRejection op et helt andet sted.

Forklaring: Du starter requesten, men du venter ikke på den. try/catch opdager kun synkrone fejl og fejl i promises, som du faktisk await‘er. Her slipper fejlen ud i det fri.

Fix: Vent på promise’et, eller returnér det videre.

async function loadData() {
  try {
    const res = await fetch('/api/data');
    console.log('Data hentet', res.ok);
  } catch (err) {
    console.error('Fejl i loadData', err);
  }
}

Eller, hvis funktionen bare skal starte noget asynkront og ikke selv vente, så skal du være bevidst om, at du laver et “fire and forget”-kald:

function fireAndForget() {
  // bevidst ikke await
  doSomethingAsync().catch(err => {
    console.error('Fire and forget fejlede', err);
  });
}

Det ser lidt grimt ud, men det er tydeligt, at du , at der kan komme en fejl, og at du håndterer den.

Fejl 2: Du tror try/catch er en magisk async-sikkerhedsnet

En af de mest snigende async/await fejl er, når du blander then-kæder og await i samme funktion.

async function saveUser(user) {
  try {
    await db.insertUser(user).then(() => {
      console.log('Bruger gemt');
      throw new Error('Noget gik galt efter insert');
    });
  } catch (err) {
    console.error('Fejl i saveUser', err);
  }
}

Symptom: Du får en UnhandledPromiseRejection eller en fejl, der ikke bliver fanget af din catch, selvom den tydeligt bliver kastet i then-blokken.

Forklaring: await venter på hele promise-kæden, ja, men hvis du laver nye promises indeni uden at returnere dem, kan du ende med fejl, der ikke bobler op til dit oprindelige await. Og blandingen gør det svært at overskue flowet.

Fix: Vælg én stil. Enten promise-kæder eller async/await. Med async/await ser samme funktion sådan her ud:

async function saveUser(user) {
  try {
    await db.insertUser(user);
    console.log('Bruger gemt');
    throw new Error('Noget gik galt efter insert');
  } catch (err) {
    console.error('Fejl i saveUser', err);
  }
}

Her bliver fejlen tydeligt fanget i catch. Ingen skjulte kæder.

Som tommelfingerregel: Hvis funktionen er async, så hold dig til await og undgå .then og .catch inde i den. Det gør både fejl og stack traces meget nemmere at læse.

Fejl 3: await i loop gør din kode unødigt langsom

Den her opdagede jeg første gang i et lille hobbyprojekt, hvor jeg hentede sprites til et pixel-art projekt én efter én. Jeg kunne se, at nettet var hurtigt, men min kode var langsom.

async function loadAll(ids) {
  const results = [];

  for (const id of ids) {
    const res = await fetch(`/api/item/${id}`);
    const data = await res.json();
    results.push(data);
  }

  return results;
}

Symptom: Koden virker, men den føles sløv. Hvis du profilerer, kan du se, at alle fetches kører efter hinanden i stedet for parallelt.

Forklaring: await i et almindeligt for-loop betyder “gør dette, vent til det er færdigt, gå til næste”. Det er fint, hvis rækkefølgen er vigtig, eller du rammer en rate limit, men ellers er det ofte spildt tid.

Fix 1: Brug Promise.all hvis alle kald er uafhængige

async function loadAll(ids) {
  const promises = ids.map(id => fetch(`/api/item/${id}`).then(r => r.json()));
  return Promise.all(promises);
}

Nu starter du alle requests med det samme og venter samlet til sidst.

Fix 2: Brug for await...of til streams eller når rækkefølge betyder noget

async function processStream(stream) {
  for await (const chunk of stream) {
    // her giver det mening at vente på hver chunk
    handleChunk(chunk);
  }
}

Hvis du vil begrænse hvor mange requests, du har i gang på én gang, kan du også lave små batches på f.eks. 5 eller 10 ad gangen. Det er lidt mere avanceret, men det er en god øvelse, hvis du vil forstå asynkron kontrol.

Fejl 4: Promise.all eksploderer på første fejl

Promise.all er skønt, lige indtil ét af dine kald fejler.

async function loadWidgets(ids) {
  const widgets = await Promise.all(
    ids.map(id => fetch(`/api/widget/${id}`).then(r => r.json()))
  );
  return widgets;
}

Symptom: Hvis bare ét endpoint fejler, ryger hele Promise.all i catch, og du får ingen af de andre resultater, selvom de egentlig lykkes.

Forklaring: Promise.all er “all or nothing”. Det resolve’er kun, hvis alle promises lykkes. Ellers rejicer den på den første fejl. Det er ofte fint, men ikke hvis du gerne vil vise det, der faktisk lykkes, og bare markere resten som fejlet.

Fix: Brug Promise.allSettled når delvise resultater er ok

async function loadWidgets(ids) {
  const results = await Promise.allSettled(
    ids.map(id => fetch(`/api/widget/${id}`).then(r => r.json()))
  );

  const ok = [];
  const failed = [];

  for (const result of results) {
    if (result.status === 'fulfilled') {
      ok.push(result.value);
    } else {
      failed.push(result.reason);
    }
  }

  return { ok, failed };
}

Nu får du et klart overblik: hvad virkede, hvad gik galt. Og du kan selv beslutte, hvordan UI’et skal reagere.

Hvis du vil læse mere om forskellen på promises og deres metoder, har MDN en ret god oversigt, som også forklarer edge cases: Promise på MDN.

Fejl 5: Race conditions med delt state

Race conditions er de fejl, der først viser sig, når du er træt, deployer til prod og 100 brugere rammer samme kode på samme tid.

let counter = 0;

async function increment() {
  const current = counter;
  await waitRandomTime();
  counter = current + 1;
}

Symptom: Du forventer, at counter bliver 10 efter 10 kald, men tallet er lavere. Eller en global variabel, cache eller “in memory” liste opfører sig uforudsigeligt.

Forklaring: Flere async-funktioner læser og skriver til den samme variabel, men de gør det forskudt i tid. De læser en gammel værdi, venter på noget, og skriver så tilbage oven på hinanden. Det ligner synkron kode, men det er det ikke.

Fix: Undgå delt, muterbar state i async flows

Nogle typiske strategier:

  • Brug immutable data og returnér nye værdier i stedet for at mutere global state
  • Samle async-opgaver i en Promise.all med lokal state
  • Lad databasen håndtere tællere og samtidighed, i stedet for at gøre det i JavaScript
async function incrementSafely(counter) {
  await waitRandomTime();
  return counter + 1;
}

async function run() {
  let counter = 0;
  const tasks = Array.from({ length: 10 }, () => incrementSafely(counter));
  const results = await Promise.all(tasks);
  counter = results[results.length - 1];
}

Det er lidt kunstigt, men pointen er: jo mindre global, delt state du har i async kode, jo færre spøgelsesbugs får du.

På backend-siden ender de her problemer tit som performance-issues eller mærkelige API-svar. Den artikel på codingclass.dk om API-grænser (“Du smadrer dit API hvis alle må hamre løs uden grænser”) hænger faktisk ret godt sammen med det her.

Fejl 6: fetch-fejl er ikke kun 500’ere

fetch er en af de største kilder til misforståelser i async/await verdenen.

async function loadProfile() {
  const res = await fetch('/api/profile');
  const data = await res.json();
  return data;
}

Symptom: Du får en mærkelig fejl som “Unexpected token < in JSON” eller “Failed to fetch”, men din server svarer fint, eller browseren viser 200 i Network-tabben.

Forklaring:

  • fetch rejicer kun på netværksfejl (timeout, CORS, ingen forbindelse). Den kaster ikke automatisk på 4xx/5xx.
  • res.json() kan kaste en fejl, hvis body ikke er valid JSON, selvom statuskoden er 200.
  • Hvis API’et sender HTML ved fejl, prøver du måske at parse en HTML-fejlside som JSON.

Fix: Tjek både res.ok og håndter JSON-fejl

async function loadProfile() {
  let res;

  try {
    res = await fetch('/api/profile');
  } catch (networkErr) {
    console.error('Netværksfejl', networkErr);
    throw new Error('Kunne ikke få forbindelse til serveren');
  }

  if (!res.ok) {
    // prøv at læse en error-besked, men forvent ikke at det altid lykkes
    let errorBody;
    try {
      errorBody = await res.json();
    } catch {
      errorBody = null;
    }

    throw new Error(errorBody?.message || `Server-fejl: ${res.status}`);
  }

  try {
    return await res.json();
  } catch (parseErr) {
    console.error('Kunne ikke parse JSON', parseErr);
    throw new Error('Ugyldigt svar fra serveren');
  }
}

Det ser mere verbost ud, ja. Men du slipper for de der aftener, hvor du stirrer på “Unexpected token <” og først en time senere opdager, at din backend sendte en HTML-fejlside.

Fejl 7: Requests der aldrig stopper (AbortController-glemmer)

Async/await gør det nemt at starte en request. At stoppe den igen er en anden historie.

Tænk en søgefunktion, der kalder fetch på hvert tastetryk. Hvis brugeren skriver hurtigt, har du potentielt 10-20 aktive requests, der alle kæmper om at opdatere UI’et.

async function search(query) {
  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
  const data = await res.json();
  renderResults(data);
}

Symptom: Resultaterne blinker, gamle svar overskriver nye, og Network-tabben i DevTools ligner en trafikulykke.

Forklaring: Hver fetch lever sit eget liv. Du har ingen måde at afbryde de gamle, og de kan stadig nå at resolve efter den nyeste, selvom de var startet tidligere.

Fix: Brug AbortController og gem controlleren et sted

let currentSearchController = null;

async function search(query) {
  // Afbryd tidligere request, hvis den findes
  if (currentSearchController) {
    currentSearchController.abort();
  }

  const controller = new AbortController();
  currentSearchController = controller;

  try {
    const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    });

    if (!res.ok) throw new Error('Serverfejl');

    const data = await res.json();

    // Kun opdater UI, hvis det stadig er den nyeste søgning
    if (controller === currentSearchController) {
      renderResults(data);
    }
  } catch (err) {
    if (err.name === 'AbortError') {
      // helt ok, vi afbrød bare
      return;
    }
    console.error('Søgning fejlede', err);
  }
}

Når du først har brugt AbortController et par gange, giver det ret god mening. Og det redder dig fra en masse UI-kaos.

Sådan læser du async stack traces uden at gå død

En lille sandhed: async/await gør stack traces lettere at læse, men kun hvis du faktisk tager dig tid til at forstå dem.

Åbn DevTools (F12), gå i Sources eller Debugger-fanen (alt efter browser), og slå “Async” call stacks til, hvis der er en mulighed for det. I Chrome ligger det typisk under tandhjulet i DevTools.

Når en async-funktion fejler, vil du ofte se noget i stil med:

Error: Noget gik galt
  at saveUser (userService.js:42)
  at async onClick (UserPage.jsx:18)

Hvis du klikker dig ned gennem stacken, kan du se:

  • Hvilken async-funktion der kastede fejlen
  • Hvilken linje der sidst blev awaited
  • Hvem der kaldte den (ofte din event-handler eller route-handler)

Du kan også sætte breakpoints lige før og efter et await for at se, hvad der faktisk ligger i dine variabler på det tidspunkt. Det føles langsomt i starten, men du lærer så meget om, hvad din kode egentlig gør.

Hvis du vil nørde mere DevTools, har Chrome-teamet en fin intro: JavaScript debugging i DevTools. Jeg vender tit tilbage til den, når jeg har glemt en genvej.

En lille async/await tjekliste til dine code reviews

Jeg kan godt lide at have en slags mini-tjekliste i baghovedet, når jeg kigger på async-kode. Noget jeg hurtigt kan scanne efter, inden jeg skriver “ser fint ud” og trykker approve.

1. Er alle promises enten awaited, returneret eller bevidst “fire and forget”?

Kig efter rå kald til async-funktioner eller promises uden await eller return. Hvis noget skal være “fire and forget”, skal der som regel være en .catch på.

2. Bliver try/catch brugt konsekvent uden blanding af .then/.catch?

Hvis du ser en async-funktion med både await og .then, er det et rødt flag. Bed om, at det bliver skrevet om til ren async/await, så fejlene bobler tydeligere op.

3. Er der await i loops, hvor vi kunne køre ting parallelt?

Hvis du ser et for– eller for...of-loop med await indeni, så spørg: “Skal det her faktisk køre i rækkefølge?” Hvis svaret er nej, så foreslå Promise.all eller en batchet løsning.

4. Håndterer fetch både statuskoder og JSON-parsing?

Tjek om der kun står await res.json() uden tjek af res.ok. Det er en klassiker. En lille hjælpefunktion til fetch kan gøre det meget pænere.

5. Er der nogen global eller delt state, som async-kode skriver til?

Global arrays, caches, counters. Alt det, hvor to async-funktioner kan skrive samtidig. Stil spørgsmålet: “Hvad sker der, hvis to kald rammer her på samme tid?”.

6. Kan lange eller gentagne requests afbrydes?

I UI-kode: søgning, live filters, autosave. Alt det, der bliver kaldt tit. Overvej om AbortController giver mening.

7. Bliver fejl enten vist til brugeren eller logget et sted?

Async-fejl, der bare bliver logget til konsollen i frontend eller droppet i backend, har det med at blive “spøgelsesbugs”. Brug en central error-håndtering eller en lille wrapper, der sikrer, at fejl ikke bare forsvinder.

Hvis du har lyst til at bygge videre på det her, kunne dit næste lille projekt være at refaktorere et gammelt stykke kode, hvor du ved, at der ligger noget rodet async-logik. Eller du kan kombinere det med logging og monitoring, som der er en fin intro til i artiklen om logs og metrics på Coding Class.

Og hvis du sidder nu og tænker “jeg har helt sikkert en await i et loop et sted”, så ja, det har du nok. Det har jeg også. Jeg finder stadig en ny cirka hver tredje måned.

Ida Balslev er den type ven, der pludselig dukker op i din messenger med et link til en lille web-app, hun lige har bygget for sjov – og bagefter gerne viser dig, hvordan du selv kan lave den. Hendes passion for kodning startede med en hjemmebygget hjemmeside til en hestestald og er langsomt vokset gennem aftener med tutorials, fejlmeldinger og små, hjemmelavede projekter.

På Codingclass.dk deler Ida den viden, hun selv manglede i starten: konkrete eksempler, tydelige forklaringer og ærlige historier om, hvad der typisk går galt første, anden og tredje gang. Hun elsker at tage et abstrakt begreb som fx "API" eller "asynkron JavaScript" og koge det ned til noget, du kan se, klikke på og lege med i browseren. For hende handler kodning ikke om at være perfekt, men om at turde prøve, bryde ting og bygge dem op igen.

Ida skriver især om webudvikling med HTML, CSS og JavaScript, små Python-scripts og grundlæggende koncepter som debugging, versionsstyring og struktur i din kode. Hun tænker altid i næste skridt: når du først forstår idéen, viser hun dig, hvordan du kan udvide det med en ekstra funktion, lidt pænere styling eller en smartere måde at tænke din kode på.

Gennem sine artikler på Codingclass.dk vil Ida gerne give dig følelsen af, at du ikke sidder alene med koden – men at der faktisk er en, der har kæmpet med de samme fejlmeddelelser og nu gerne vil vise dig en vej igennem dem, i et tempo hvor alle kan være med.

1 kommentar

comments user
Kristoffer Lund

Enig, haha🙂 min mentale model var sååå forkert da jeg lavede spejderhjemmeside Hvordan finder man uafventede promises?

Send kommentar

You May Have Missed