// 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.
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.
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
| Goal | Pattern |
|---|---|
| Wait for one promise | const x = await p; |
| Handle errors | try { await ... } catch (e) { ... } |
| Run independent tasks in parallel | await Promise.all([...]) |
| Sequential loop | for...of with await |
| Concurrent loop | map → Promise.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.