JavaScript Async/Await
async and await are syntactic sugar on top of promises. They let you write asynchronous code that reads like synchronous code: top-to-bottom, with normal try/catch for errors. No nested .then chains, no separate error path. If you’ve internalised the promise model from chapter 40, async/await is a tiny conceptual step with an enormous readability payoff.
The Two Keywords
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
return user;
}
const ada = await loadUser(1);
console.log(ada);
asyncbefore a function makes it return a promise. Whatever youreturnbecomes the resolved value; whatever youthrowbecomes the rejection.awaitbefore a promise pauses the async function until the promise settles, then unwraps it. If the promise rejects,awaitthrows that reason.
What async Really Does
async function a() { return 1; }
async function b() { return Promise.resolve(2); }
async function c() { throw new Error("nope"); }
a().then(v => console.log(v)); // 1
b().then(v => console.log(v)); // 2 (auto-unwrapped)
c().catch(e => console.error(e)); // Error: nope
Equivalents:
async function f() { return x; }
// roughly:
function f() { return Promise.resolve(x); }
async function g() { throw err; }
// roughly:
function g() { return Promise.reject(err); }
The function body still runs synchronously until the first await.
await Pauses; The Event Loop Continues
await doesn’t block the thread. It returns control to the event loop. When the awaited promise settles, the rest of the function is scheduled as a microtask.
async function example() {
console.log("A");
await Promise.resolve();
console.log("B");
}
console.log("1");
example();
console.log("2");
// Output:
// 1
// A ← runs sync up to the first await
// 2 ← caller continues
// B ← microtask after the awaited promise settles
This is exactly the model from chapter 38 — await is Promise.then(continuation) under the hood.
Error Handling — Real try/catch
async function loadDashboard() {
try {
const user = await fetchUser(1);
const orders = await fetchOrders(user.id);
render(user, orders);
} catch (err) {
showError(err);
} finally {
spinner.hide();
}
}
One try, one catch. Compare with the equivalent .then chain:
function loadDashboard() {
return fetchUser(1)
.then(user => fetchOrders(user.id).then(orders => render(user, orders)))
.catch(showError)
.finally(() => spinner.hide());
}
The promise version isn’t terrible — but await scales to complex flows (branching, conditionals, loops) far better.
Sequential vs Parallel — The #1 Pitfall
await is sequential. If two awaits don’t depend on each other, you’re wasting time:
// ❌ Sequential — slow
async function load() {
const user = await fetchUser(1); // 200 ms
const posts = await fetchPosts(1); // 200 ms — total 400 ms
}
// ✅ Parallel — start both, then await
async function load() {
const userP = fetchUser(1); // starts immediately
const postsP = fetchPosts(1); // starts immediately
const user = await userP;
const posts = await postsP; // total ~200 ms
}
// ✅ Even cleaner with Promise.all
async function load() {
const [user, posts] = await Promise.all([fetchUser(1), fetchPosts(1)]);
}
Tip: Whenever you see two consecutive awaits for unrelated promises, that’s a chance to parallelise.
Loops With await
The right loop matters.
// ✅ Sequential — process one at a time
for (const item of items) {
await save(item);
}
// ❌ forEach does NOT await
items.forEach(async item => await save(item)); // all start in parallel; outer fn returns immediately
// ✅ Parallel, all at once
await Promise.all(items.map(item => save(item)));
// ✅ Limited concurrency (3 at a time) — when "all at once" is too aggressive
async function parallel(items, limit, fn) {
const results = [];
const executing = [];
for (const item of items) {
const p = Promise.resolve().then(() => fn(item));
results.push(p);
if (limit <= items.length) {
const e = p.finally(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) await Promise.race(executing);
}
}
return Promise.all(results);
}
for await...of — Async Iterables
For streams and generators that yield promises, for await...of is the right loop:
async function* batches() {
yield await fetch("/page/1").then(r => r.json());
yield await fetch("/page/2").then(r => r.json());
}
for await (const page of batches()) {
console.log(page);
}
Works with ReadableStream, async generators (chapter 49), and any object implementing Symbol.asyncIterator.
Top-Level await
In ES modules (and the Node REPL) you can use await directly at the top level — no wrapping async function needed:
// module.mjs
const config = await fetch("/config.json").then(r => r.json());
console.log(config);
In a regular <script> (non-module) you still need an async wrapper:
(async () => {
const config = await fetch("/config.json").then(r => r.json());
console.log(config);
})();
Mixing await and Promise Methods
You can — and often should:
// Parallel + await result
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
// Timeout via race
const result = await Promise.race([
doWork(),
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000)),
]);
// Survive partial failures
const results = await Promise.allSettled(items.map(load));
const ok = results.filter(r => r.status === "fulfilled").map(r => r.value);
Returning Vs Awaiting
return await x is almost identical to return x inside an async function. Differences:
return xreturns the promise directly to the callerreturn await xwaits for it inside this function, then returns its value
The visible difference: error-handling boundaries. return await x inside try will catch the rejection here; return x inside try will not.
async function inside() {
try {
return await failing(); // catches here
} catch (e) {
return "fallback";
}
}
async function outside() {
try {
return failing(); // ❌ does NOT catch — rejection bubbles to caller
} catch (e) {
return "fallback";
}
}
Tip: Use return await inside try blocks so the local catch actually catches the rejection.
Cancellation — AbortController
Same pattern as chapter 40 — await doesn’t change cancellation, you still need an AbortSignal:
async function loadWithTimeout(url, ms = 5000) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), ms);
try {
const res = await fetch(url, { signal: ctrl.signal });
return await res.json();
} finally {
clearTimeout(timer);
}
}
A Realistic Example — Pipeline With Recovery
async function publishArticle(article) {
try {
const draft = await db.saveDraft(article);
// Run independent steps in parallel
const [validated, indexed] = await Promise.all([
validator.check(draft),
search.index(draft),
]);
if (!validated.ok) {
throw new Error(`Validation failed: ${validated.errors.join(", ")}`);
}
const published = await db.markPublished(draft.id);
await notifier.notify({ event: "article:published", id: published.id });
return { ok: true, id: published.id };
} catch (err) {
console.error("publish failed", err);
await rollback(article).catch(rbErr => console.error("rollback also failed", rbErr));
return { ok: false, error: err.message };
}
}
Sequential where ordering matters, parallel where it doesn’t, single try/catch, structured return.
Common Mistakes
// ❌ Forgetting async
function load() {
const x = await fetchX(); // SyntaxError — await outside async
}
// ❌ Forgetting await — `result` is a Promise, not the value
async function f() {
const result = fetchX();
console.log(result); // [object Promise]
}
// ❌ Awaiting in a tight serial loop when parallel is fine
for (const id of ids) await fetchUser(id); // slow
// ✅
const users = await Promise.all(ids.map(fetchUser));
// ❌ Swallowing errors silently
async function f() {
try { await dangerous(); } catch {} // empty catch = silent failure
}
// ❌ Async constructors don't exist
class C {
async constructor() {} // ❌ SyntaxError
}
// ✅ Static factory:
class C {
static async create() { const c = new C(); await c.init(); return c; }
}
Best Practices Checklist
- ✅ Reach for
async/awaitover manual.thenchains in new code - ✅ Use one
try/catchper logical block, not perawait - ✅ Start independent promises before awaiting (or use
Promise.all) - ✅ Use
for…of(orfor await…of) — neverforEach— when awaiting per item - ✅ Use
return awaitinsidetryso localcatchactually runs - ✅ Use
AbortControllerfor cancellation/timeouts - ❌ Don’t await unrelated promises in sequence
- ❌ Don’t catch errors silently
- ❌ Don’t forget to
await— silentPromisereturns lurk in your data
What’s Next?
Async fluency unlocked. Time for the most common use of it on the web — the Fetch API — for talking to HTTP servers. Next chapter covers requests, responses, headers, JSON, errors, and cancellation.