</> HTML5Advent
ENFRESDEITPT

// js · Web Platform Advent #3

async/await in JavaScript: a practical guide

How async and await work, how they relate to promises, error handling with try/catch, awaiting in loops, and the serial-vs-parallel gotcha - with runnable examples.

A developer's hands typing on a keyboard with colourful source code on the monitor

The async and await keywords are syntax over promises. They let you write asynchronous code that reads top-to-bottom like synchronous code, while still being non-blocking under the hood. If you know promises, you already know async/await - it just removes the .then() ceremony.

The basics

Mark a function async and you can use await inside it. await pauses that function until the promise settles, then gives you the resolved value:

async function getUser(id) {
  const res = await fetch('/api/user/' + id);
  const user = await res.json();
  return user.name;
}

getUser(42).then((name) => console.log(name));

Two things to remember: an async function always returns a promise (whatever you return is wrapped in one), and await only works inside an async function - except at the top level of an ES module, where top-level await is allowed.

async/await vs promises

They're the same machinery. This promise chain…

fetch('/api/user')
  .then((res) => res.json())
  .then((user) => console.log(user.name));

…is exactly equivalent to this async version, which most people find easier to read and debug:

const res = await fetch('/api/user');
const user = await res.json();
console.log(user.name);

You can mix them freely - await any promise, including the result of Promise.all().

Error handling with try/catch

With async/await you catch rejected promises using ordinary try/catch, the same construct you use for synchronous errors:

async function loadUser(id) {
  try {
    const res = await fetch('/api/user/' + id);
    if (!res.ok) throw new Error('HTTP ' + res.status);
    return await res.json();
  } catch (err) {
    console.error('Could not load user:', err.message);
    return null;
  } finally {
    console.log('loadUser finished');
  }
}

A rejected await throws an exception at that line, so the catch block runs. The finally block runs whether or not an error occurred.

A multi-lane motorway with cars and trucks travelling side by side in parallel
Independent tasks can run side by side like vehicles in parallel lanes - awaiting them one at a time forces a single-file queue instead.

The big gotcha: serial vs parallel await

The most common async/await mistake is awaiting independent tasks one after another. This runs in series - the second request doesn't start until the first finishes:

// Slow: total time = a + b + c
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();

If the tasks don't depend on each other, start them all first, then await together with Promise.all. Now they run in parallel:

// Fast: total time ≈ the slowest one
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);

await in loops

A plain for loop with await processes items one at a time, which is correct when each step depends on the previous one or you must avoid overwhelming a server:

for (const id of ids) {
  const item = await fetchItem(id);   // sequential - one after another
  console.log(item);
}

To process them concurrently, map to an array of promises and await Promise.all. Note that forEach does not wait for async callbacks - use map + Promise.all instead:

const items = await Promise.all(ids.map((id) => fetchItem(id)));

Quick reference

GoalPattern
Wait for one promiseconst x = await p;
Handle errorstry { await ... } catch (e) { ... }
Run independent tasks in parallelawait Promise.all([...])
Sequential loopfor...of with await
Concurrent loopmapPromise.all

async/await doesn't replace promises - it makes them readable. Keep Promise.all in your toolkit for parallelism, reach for try/catch for errors, and watch out for the accidental serial await, and you'll have asynchronous JavaScript well in hand.

Frequently asked questions

What is async/await in JavaScript?
async/await is syntax that lets you write promise-based asynchronous code as if it were synchronous. Marking a function async makes it return a promise, and await pauses inside that function until a promise settles, so you read the logic top to bottom instead of chaining .then calls.
Is async/await better than promises?
They're the same thing underneath - async/await is built on promises and just reads more clearly for sequential steps and try/catch error handling. Promise combinators like Promise.all are still the right tool for running work in parallel, so you mix both.
How do you handle errors with async/await?
Wrap the awaited calls in a try/catch block; a rejected promise throws at the await, so catch handles it like any synchronous error. From the caller side you can also add a .catch on the promise the async function returns.
Why is awaiting in a loop slow?
Awaiting each item one after another runs them serially - each waits for the previous to finish. If the tasks are independent, start them all first and await Promise.all on the array instead, which runs them in parallel and is far faster.