async & await
async and await let you write promise-based code that reads like ordinary step-by-step code. They don't replace promises — they're a nicer way to use them — and they make error handling with try/catch work naturally across asynchronous steps.
async functions
Putting async before a function makes it always return a promise. Whatever you return becomes the fulfilled value; anything you throw becomes a rejection:
async function getNumber() { return 42; } async function fail() { throw new Error("nope"); } getNumber().then(n => console.log("got", n)); fail().catch(err => console.log("caught", err.message)); console.log(getNumber() instanceof Promise);
true got 42 caught nope
await
Inside an async function, await pauses that function until a promise settles, then gives you its value:
const wait = (ms, value) => new Promise(r => setTimeout(() => r(value), ms)); async function makeBreakfast() { console.log("start toast"); const toast = await wait(20, "🍞 toast"); console.log("toast ready, start eggs"); const eggs = await wait(20, "🍳 eggs"); return `${toast} + ${eggs}`; } makeBreakfast().then(meal => console.log("served:", meal)); console.log("(the rest of the program keeps running)");
start toast (the rest of the program keeps running) toast ready, start eggs served: 🍞 toast + 🍳 eggs
await doesn't freeze the whole program — only the async function it's in. Other code keeps running in the meantime.
Error handling with try/catch
const fetchUser = id => new Promise((resolve, reject) => setTimeout(() => (id > 0 ? resolve({ id, name: "Ana" }) : reject(new Error("User not found"))), 10)); async function showUser(id) { try { const user = await fetchUser(id); console.log("Hello,", user.name); } catch (err) { console.log("Problem:", err.message); } finally { console.log("(hide spinner)"); } } await showUser(1); await showUser(-1);
Hello, Ana (hide spinner) Problem: User not found (hide spinner)
A rejected promise makes await throw, so one try/catch covers every awaited step. This example also uses top-level await, which works in ES modules (and in the browser console) without wrapping code in an async function.
Sequential vs parallel
Awaiting one thing after another is right when each step depends on the previous one. When steps are independent, awaiting them in sequence wastes time:
const wait = (ms, v) => new Promise(r => setTimeout(() => r(v), ms)); async function sequential() { const t = Date.now(); const a = await wait(50, "a"); const b = await wait(50, "b"); return `${a}${b} in ~${Math.round((Date.now() - t) / 50) * 50}ms`; } async function parallel() { const t = Date.now(); const [a, b] = await Promise.all([wait(50, "a"), wait(50, "b")]); return `${a}${b} in ~${Math.round((Date.now() - t) / 50) * 50}ms`; } console.log("sequential:", await sequential()); console.log("parallel: ", await parallel());
sequential: ab in ~100ms parallel: ab in ~50ms
Start independent operations first, then await them together with Promise.all. This is one of the most valuable performance habits in JavaScript.
await in loops
const wait = (ms, v) => new Promise(r => setTimeout(() => r(v), ms)); const ids = [1, 2, 3]; for (const id of ids) { const result = await wait(10, `item ${id}`); console.log("sequential:", result); } const all = await Promise.all(ids.map(id => wait(10, `item ${id}`))); console.log("parallel:", all.join(", "));
sequential: item 1 sequential: item 2 sequential: item 3 parallel: item 1, item 2, item 3
Use for…of with await when order or rate limits matter; use map + Promise.all for speed. Don't use await inside forEach — forEach ignores the returned promises, so it won't wait.
Async iteration
for await…of loops over async iterables, such as an async generator that fetches pages one at a time:
const wait = (ms, v) => new Promise(r => setTimeout(() => r(v), ms)); async function* pages() { for (let page = 1; page <= 3; page++) { yield await wait(10, [`row ${page}a`, `row ${page}b`]); } } for await (const rows of pages()) { console.log(rows.join(" | ")); }
row 1a | row 1b row 2a | row 2b row 3a | row 3b
A realistic example
The second request needs the first one's result, so they're awaited in sequence. Errors from either step land in the same catch.