CodeOath
← All posts
JavaScript75 min total · 15 parts

JavaScript Core Concepts: Scope, Closures, `this`, and the Event Loop

Contents — Part 11 of 15: async/await: Promises With Synchronous-Looking Syntax
Part 11 of 15 · ~2 min

async/await: Promises With Synchronous-Looking Syntax

async/await doesn't introduce a new asynchronous mechanism — it's syntax sugar over Promises that makes asynchronous code read top-to-bottom like synchronous code.

async function loadUser(id) {
  const res = await fetch(`/api/users/${id}`); // pauses THIS function until the Promise settles
  const user = await res.json();
  return user; // an async function always returns a Promise, wrapping this return value
}

await only pauses the async function it's inside — it does not block the rest of the program, since the JavaScript engine is free to run other code (other event handlers, other Promise callbacks) while this function is paused waiting.

A common gotcha: forgetting to await

async function loadData() {
  await fetch("/api/data");
  console.log("data loaded");
}
loadData();
console.log("this runs first"); // logs before "data loaded", regardless of fetch speed

Calling an async function without await-ing it doesn't pause the caller — execution in the caller continues immediately, and whatever's inside the async function resumes later. This is correct behavior, not a bug, but it's a frequent source of "why did this log out of order" confusion.

A common bug: sequential await where concurrent would do

// Slow — the second fetch doesn't even START until the first one fully finishes
async function loadBoth(userId, postId) {
  const user = await fetchUser(userId);
  const post = await fetchPost(postId);
  return { user, post };
}

// Fast — both requests fire at the same time, total time ≈ the slower of the two, not the sum
async function loadBothFast(userId, postId) {
  const [user, post] = await Promise.all([fetchUser(userId), fetchPost(postId)]);
  return { user, post };
}

Two independent await calls in sequence run one after another even though nothing requires it — a very common, easy-to-miss performance bug. Use Promise.all whenever the requests don't actually depend on each other's results.

Error handling with try/catch

async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`Request failed: ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error("Could not load user:", err);
    throw err; // re-throw if the caller also needs to know
  }
}

A rejected awaited Promise throws inside the async function exactly like a synchronous throw would, which is why ordinary try/catch works for async errors — no special async-specific error-handling syntax is needed.