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:

JS async-fn.js
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);
Output
true
got 42
caught nope

await

Inside an async function, await pauses that function until a promise settles, then gives you its value:

JS await.js
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)");
Output
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

JS try-catch.js
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);
Output
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:

JS parallel.js
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());
Output
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

JS loops.js
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(", "));
Output
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:

JS for-await.js
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(" | "));
}
Output
row 1a | row 1b
row 2a | row 2b
row 3a | row 3b

A realistic example

Try it yourself
Result

The second request needs the first one's result, so they're awaited in sequence. Errors from either step land in the same catch.

Try it yourself

Try it yourself
Console output