Episode 1 — Fundamentals / 1.17 — Asynchronous Programming JavaScript

1.17.d — Async/Await

In one sentence: async/await is syntactic sugar over Promises — an async function always returns a Promise, and await pauses only that function's execution until the awaited Promise settles, making async code read like synchronous code.

Navigation: ← 1.17.c — Understanding Promises · 1.17.e — Timers →


1. The async function declaration

Adding async before a function makes it always return a Promise:

async function greet() {
  return "Hello";
}

// Equivalent to:
function greet() {
  return Promise.resolve("Hello");
}

greet().then(function (msg) {
  console.log(msg); // "Hello"
});

Even if you return a plain value, it is automatically wrapped in Promise.resolve().

async function getNumber() {
  return 42;
}

getNumber().then(console.log); // 42

If you throw inside an async function, the returned Promise rejects:

async function fail() {
  throw new Error("Something broke");
}

fail().catch(function (err) {
  console.error(err.message); // "Something broke"
});

Different forms of async functions

// Function declaration
async function fetchData() { /* ... */ }

// Function expression
const fetchData = async function () { /* ... */ };

// Arrow function
const fetchData = async () => { /* ... */ };

// Method in an object
const api = {
  async getData() { /* ... */ }
};

// Method in a class
class Api {
  async getData() { /* ... */ }
}

2. The await keyword

await can only be used inside an async function (or at the top level of a module — see section 5). It pauses the function's execution until the Promise settles.

async function fetchUser() {
  console.log("Fetching...");
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  console.log("Got response");        // runs after fetch resolves
  const user = await response.json(); // waits for JSON parsing
  console.log("User:", user.name);
  return user;
}

fetchUser();
console.log("This runs BEFORE the user is fetched");

Output order:

Fetching...
This runs BEFORE the user is fetched
Got response
User: Leanne Graham

Key insight: await pauses the async function, not the entire thread. Code outside the function continues running.


3. Error handling with try...catch

Instead of .catch(), use try...catch for a synchronous-looking error flow:

async function loadUser(userId) {
  try {
    const response = await fetch("/api/users/" + userId);

    if (!response.ok) {
      throw new Error("HTTP " + response.status);
    }

    const user = await response.json();
    console.log("User loaded:", user.name);
    return user;

  } catch (err) {
    console.error("Failed to load user:", err.message);
    // Optionally re-throw or return a default
    return null;
  } finally {
    console.log("Fetch attempt finished"); // always runs
  }
}

Comparison: Promise chain vs async/await

// Promise chain
function getUser(id) {
  return fetch("/api/users/" + id)
    .then(function (res) {
      if (!res.ok) throw new Error("HTTP " + res.status);
      return res.json();
    })
    .then(function (user) {
      console.log(user.name);
      return user;
    })
    .catch(function (err) {
      console.error(err.message);
      return null;
    });
}

// Async/await — same logic, reads top to bottom
async function getUser(id) {
  try {
    const res = await fetch("/api/users/" + id);
    if (!res.ok) throw new Error("HTTP " + res.status);
    const user = await res.json();
    console.log(user.name);
    return user;
  } catch (err) {
    console.error(err.message);
    return null;
  }
}

4. Sequential vs parallel execution

4.1 Sequential (one after another)

Each await waits for the previous to finish. Total time = sum of all delays.

async function sequential() {
  const user = await fetchUser(1);       // 1 second
  const posts = await fetchPosts(1);     // 1 second
  const comments = await fetchComments(1); // 1 second
  // Total: ~3 seconds
  return { user, posts, comments };
}

4.2 Parallel (all at once)

Start all Promises first, then await them together with Promise.all. Total time = longest single operation.

async function parallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(1),       // 1 second ─┐
    fetchPosts(1),      // 1 second ─┤ all run concurrently
    fetchComments(1),   // 1 second ─┘
  ]);
  // Total: ~1 second (not 3!)
  return { user, posts, comments };
}

4.3 When to use which

PatternUse when
SequentialEach step depends on the previous step's result
ParallelSteps are independent of each other
MixedSome steps depend on earlier results, others do not
// Mixed: fetch user first (needed for posts), then posts and profile in parallel
async function mixed() {
  const user = await fetchUser(1);                   // must be first
  const [posts, profile] = await Promise.all([
    fetchPosts(user.id),                              // needs user.id
    fetchProfile(user.id),                            // needs user.id, but independent of posts
  ]);
  return { user, posts, profile };
}

5. Top-level await (ES2022)

In ES modules (files with type="module" or .mjs extension), you can use await outside any function:

<script type="module">
  const response = await fetch("/api/config");
  const config = await response.json();
  console.log("Config loaded:", config);
</script>
// config.mjs (Node.js with "type": "module" in package.json)
const data = await fetch("https://api.example.com/settings");
export const settings = await data.json();

Limitations:

  • Only works in ES modules, not in classic scripts or CommonJS.
  • The module's evaluation pauses until the await resolves — importers of that module also wait.
  • Use cautiously: a slow top-level await delays the entire module graph.

6. Converting promise chains to async/await

Before (Promise chain)

function processOrder(orderId) {
  return getOrder(orderId)
    .then(function (order) {
      return validateOrder(order);
    })
    .then(function (validOrder) {
      return chargePayment(validOrder);
    })
    .then(function (receipt) {
      return sendConfirmation(receipt);
    })
    .then(function (confirmation) {
      console.log("Order complete:", confirmation.id);
      return confirmation;
    })
    .catch(function (err) {
      console.error("Order failed:", err.message);
      throw err;
    });
}

After (async/await)

async function processOrder(orderId) {
  try {
    const order = await getOrder(orderId);
    const validOrder = await validateOrder(order);
    const receipt = await chargePayment(validOrder);
    const confirmation = await sendConfirmation(receipt);
    console.log("Order complete:", confirmation.id);
    return confirmation;
  } catch (err) {
    console.error("Order failed:", err.message);
    throw err;
  }
}

7. Common pitfalls

Pitfall 1: Forgetting await

async function broken() {
  const data = fetch("/api/data");   // missing await!
  console.log(data);                  // logs Promise { <pending> }, not the response
}

async function fixed() {
  const data = await fetch("/api/data");
  console.log(data);                  // logs the Response object
}

Pitfall 2: await in forEach does not work as expected

forEach does not wait for async callbacks — it fires them all and moves on.

// WRONG — all requests fire simultaneously, forEach does not await
async function wrong() {
  const ids = [1, 2, 3];
  ids.forEach(async function (id) {
    const user = await fetchUser(id);
    console.log(user.name);
  });
  console.log("Done");  // prints BEFORE any user is logged
}

// CORRECT — use for...of for sequential processing
async function sequential() {
  const ids = [1, 2, 3];
  for (const id of ids) {
    const user = await fetchUser(id);
    console.log(user.name);
  }
  console.log("Done");  // prints AFTER all users are logged
}

// CORRECT — use Promise.all + map for parallel processing
async function parallel() {
  const ids = [1, 2, 3];
  const users = await Promise.all(
    ids.map(function (id) { return fetchUser(id); })
  );
  users.forEach(function (user) { console.log(user.name); });
  console.log("Done");
}

Pitfall 3: Unnecessary sequential awaits

// SLOW — these two fetches are independent but run sequentially
async function slow() {
  const users = await fetch("/api/users");      // waits ~500ms
  const products = await fetch("/api/products"); // waits ~500ms after users finish
  // Total: ~1000ms
}

// FAST — run in parallel
async function fast() {
  const [users, products] = await Promise.all([
    fetch("/api/users"),
    fetch("/api/products"),
  ]);
  // Total: ~500ms
}

Pitfall 4: Unhandled rejections

// DANGEROUS — if fetchUser rejects, nothing catches it
async function noErrorHandling() {
  const user = await fetchUser(-1); // throws!
  console.log(user);
}

noErrorHandling(); // Unhandled promise rejection

// SAFE — always handle errors
noErrorHandling().catch(console.error);

// OR: use try...catch inside the function

8. Real-world pattern: fetching data from an API

async function displayUserDashboard(userId) {
  const dashboard = document.getElementById("dashboard");
  const spinner = document.getElementById("spinner");

  spinner.style.display = "block";

  try {
    // Fetch user data and their recent activity in parallel
    const [userRes, activityRes] = await Promise.all([
      fetch("/api/users/" + userId),
      fetch("/api/users/" + userId + "/activity"),
    ]);

    // Check both responses
    if (!userRes.ok) throw new Error("Failed to load user");
    if (!activityRes.ok) throw new Error("Failed to load activity");

    // Parse JSON in parallel
    const [user, activities] = await Promise.all([
      userRes.json(),
      activityRes.json(),
    ]);

    // Render the dashboard
    dashboard.innerHTML = "<h1>" + user.name + "</h1>";
    activities.forEach(function (act) {
      const p = document.createElement("p");
      p.textContent = act.description + " — " + act.date;
      dashboard.appendChild(p);
    });

  } catch (err) {
    dashboard.innerHTML = "<p class='error'>Error: " + err.message + "</p>";
  } finally {
    spinner.style.display = "none";
  }
}

displayUserDashboard(42);

Key takeaways

  1. async functions always return a Promise — even if you return a plain value.
  2. await pauses only the async function, not the thread — outside code keeps running.
  3. Use try...catch inside async functions for clean, readable error handling.
  4. Independent operations should use Promise.all for parallel execution, not sequential await.
  5. await in forEach does not work — use for...of (sequential) or Promise.all + map (parallel).
  6. Top-level await works only in ES modules (ES2022).
  7. Always handle errors — either with try...catch inside or .catch() on the returned Promise.

Explain-It Challenge

Explain without notes:

  1. What does an async function return if you write return 5?
  2. Why is await inside forEach a common bug? What should you use instead?
  3. How would you run three independent API calls in parallel and wait for all results?

Navigation: ← 1.17.c — Understanding Promises · 1.17.e — Timers →