Skip to content
elephantoo

async & await

Lesson 28 of 34 16 min read

Write asynchronous code that reads top-to-bottom, handle errors, and run tasks in parallel.


async and await are syntax built on top of promises that lets asynchronous code read like ordinary top-to-bottom code. There's nothing new underneath — just promises — but the result is far easier to write, read and debug. Most modern JavaScript uses async/await for anything asynchronous.

From .then to await#

Here's a promise chain:

JavaScript
function getUserName(id) {
  return fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    .then((res) => res.json())
    .then((user) => user.name);
}

And the same logic with async/await:

JavaScript
async function getUserName(id) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  const user = await res.json();
  return user.name;
}

getUserName(2).then((name) => console.log(name)); // Ervin Howell
  • async before a function makes it always return a promise.
  • await pauses that function until the promise settles, then gives you the fulfilled value (or throws the rejection reason).
  • While it's paused, the rest of the program keeps running — await doesn't block the thread.

async functions return promises#

JavaScript
async function getNumber() {
  return 42;
}

const result = getNumber();
console.log(result instanceof Promise); // true
result.then((n) => console.log(n));     // 42

async function fail() {
  throw new Error("Oops");
}
fail().catch((err) => console.log("Caught:", err.message)); // Caught: Oops

Returning a value fulfils the promise; throwing rejects it. Every form of function can be async:

JavaScript
const arrow = async (x) => x * 2;
const obj = {
  async load() {
    return "loaded";
  },
};
class Repo {
  async find(id) {
    return { id };
  }
}

Error handling with try/catch#

The big win: ordinary try/catch/finally works with asynchronous code:

JavaScript
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function fetchScore(student) {
  await wait(100);
  if (student === "nobody") throw new Error(`No student called ${student}`);
  return { student, score: 91 };
}

async function showScore(student) {
  console.log(`Loading ${student}…`);
  try {
    const { score } = await fetchScore(student);
    console.log(`${student} scored ${score}`);
  } catch (err) {
    console.log(`Error: ${err.message}`);
  } finally {
    console.log("Spinner hidden");
  }
}

await showScore("ada");
await showScore("nobody");
Output
Loading ada…
ada scored 91
Spinner hidden
Loading nobody…
Error: No student called nobody
Spinner hidden

These examples use top-level await, which works in ES modules: save as .mjs (or use "type": "module") to run with Node, or use a <script type="module"> in the browser. In a regular script, wrap the code in an async function.

Sequential vs parallel#

await waits for each promise in turn. That's right when one step needs the previous step's result:

JavaScript
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

// Sequential – each step depends on the last (~200 ms total)
const user = await wait(100, { id: 1 });
const orders = await wait(100, [`order for user ${user.id}`]);
console.log(orders);

But for independent work, awaiting one after another wastes time:

JavaScript
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

console.time("sequential");
const a = await wait(300, "profile");
const b = await wait(300, "courses");
console.timeEnd("sequential"); // ~600 ms

console.time("parallel");
const [p, c] = await Promise.all([wait(300, "profile"), wait(300, "courses")]);
console.timeEnd("parallel"); // ~300 ms
console.log(p, c); // profile courses

Start everything that doesn't depend on something else, then await them together. Use Promise.allSettled when some may fail and you still want the rest.

Loops and await#

Sequential: for...of

JavaScript
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const ids = [1, 2, 3];

for (const id of ids) {
  const item = await wait(50, `item ${id}`);
  console.log(item); // item 1, item 2, item 3 – one at a time, in order
}

Parallel: map + Promise.all

JavaScript
const items = await Promise.all(ids.map((id) => wait(50, `item ${id}`)));
console.log(items); // [ 'item 1', 'item 2', 'item 3' ]

The forEach trap

JavaScript
const saved = [];
ids.forEach(async (id) => {
  await wait(50);
  saved.push(id);
});
console.log(saved); // [] – forEach didn't wait!

forEach calls the async callback and ignores the promise it returns. Use for...of or Promise.all instead.

Limiting concurrency

Firing 1,000 requests at once can overwhelm a server (or get you rate-limited). Process in batches:

JavaScript
async function inBatches(items, size, worker) {
  const results = [];
  for (let i = 0; i < items.length; i += size) {
    const batch = items.slice(i, i + size);
    results.push(...(await Promise.all(batch.map(worker))));
  }
  return results;
}

const doubled = await inBatches([1, 2, 3, 4, 5], 2, async (n) => {
  await wait(20);
  return n * 2;
});
console.log(doubled); // [ 2, 4, 6, 8, 10 ]

Retrying with back-off#

A common real-world helper — retry a flaky operation, waiting longer each time:

JavaScript
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function retry(fn, { attempts = 3, delay = 100 } = {}) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await fn(attempt);
    } catch (err) {
      if (attempt >= attempts) throw err;
      console.log(`Attempt ${attempt} failed (${err.message}); retrying in ${delay} ms`);
      await sleep(delay);
      delay *= 2; // exponential back-off
    }
  }
}

const value = await retry(async (attempt) => {
  if (attempt < 3) throw new Error("server busy");
  return "success!";
});
console.log(value);
Output
Attempt 1 failed (server busy); retrying in 100 ms
Attempt 2 failed (server busy); retrying in 200 ms
success!

Note return await fn() inside try: the await is needed so a rejection is caught by this catch.

Cancelling with AbortController#

Promises can't be cancelled by themselves, but many APIs (including fetch) accept an AbortSignal:

JavaScript
const controller = new AbortController();

const sleepCancellable = (ms, signal) =>
  new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms);
    signal.addEventListener("abort", () => {
      clearTimeout(id);
      reject(signal.reason);
    }, { once: true });
  });

setTimeout(() => controller.abort(new Error("User navigated away")), 100);

try {
  await sleepCancellable(1000, controller.signal);
} catch (err) {
  console.log("Cancelled:", err.message); // Cancelled: User navigated away
}

You'll use this pattern for search-as-you-type and React effects.

Don't forget to await#

JavaScript
async function save() {
  throw new Error("disk full");
}

async function bad() {
  try {
    save(); // ❌ no await – the rejection escapes the try/catch
  } catch {
    console.log("never runs");
  }
}

async function good() {
  try {
    await save(); // ✅
  } catch (err) {
    console.log("Handled:", err.message);
  }
}

await good(); // Handled: disk full

Forgetting await is the most common async bug. Linters (ESLint's no-floating-promises with TypeScript) can catch it for you.

Common mistakes#

  • Forgetting await, so you get a Promise { <pending> } instead of a value.
  • Awaiting independent tasks one by one instead of using Promise.all.
  • Using await inside forEach, map or filter callbacks and expecting the loop to wait.
  • Using await in a non-async function: SyntaxError: await is only valid in async functions and the top level bodies of modules.
  • Swallowing errors with an empty catch.

What's next#

Now put async/await to work talking to real servers with fetch, APIs and JSON.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What does an async function always return?

  2. 2.Two independent requests are written as const a = await getA(); const b = await getB();. How can you make them run at the same time?

  3. 3.Why doesn't items.forEach(async (item) => { await save(item); }) wait for all saves to finish?

Finished reading?

Mark this lesson complete to track your progress.