Home Module 14 Async / Await

1. Introduction

async/await is syntactic sugar over Promises. It lets you write async code that looks and reads like regular synchronous code — no .then() chains, no callbacks. Under the hood it still uses Promises, so everything you learned in Lesson 2 applies. async/await is the dominant pattern in modern JavaScript.

2. Theory

2.1 The async keyword

// async marks a function as asynchronous
// It always returns a Promise, even if you return a plain value
async function greet(name) {
  return `Hello, ${name}!`; // wrapped in Promise.resolve() automatically
}

const result = greet('Alice');
console.log(result); // Promise { 'Hello, Alice!' }

result.then(msg => console.log(msg)); // "Hello, Alice!"

// async arrow function
const square = async n => n * n;

// async method
class Api {
  async getUser(id) { ... }
}

2.2 The await keyword

// await pauses execution of the async function until the Promise settles
// It can only be used INSIDE an async function

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function example() {
  console.log('Start');
  await delay(1000); // pause for 1 second
  console.log('After 1 second');
  await delay(500);
  console.log('After 0.5 more seconds');
}

example();
// Output: "Start" → (1s) → "After 1 second" → (0.5s) → "After 0.5 more seconds"
// Other code can run during the waits!

2.3 async/await vs .then() — same result

// Promise chain
function loadData() {
  return fetchUser(1)
    .then(user    => fetchProfile(user.id))
    .then(profile => fetchPosts(profile.id))
    .then(posts   => posts);
}

// async/await — identical behaviour, much more readable
async function loadData() {
  const user    = await fetchUser(1);
  const profile = await fetchProfile(user.id);
  const posts   = await fetchPosts(profile.id);
  return posts;
}

2.4 Error handling with try/catch

// .then() approach
fetchUser(1)
  .then(user => processUser(user))
  .catch(err => console.error(err));

// async/await — use regular try/catch
async function handleUser() {
  try {
    const user = await fetchUser(1);
    const result = await processUser(user);
    return result;
  } catch (err) {
    console.error('Error:', err.message);
    return null; // or re-throw: throw err;
  } finally {
    console.log('Always runs');
  }
}

// Multiple specific error types
async function loadResource(id) {
  try {
    const user = await fetchUser(id);
    return user;
  } catch (err) {
    if (err.message.includes('not found')) {
      return null; // expected — handle gracefully
    }
    throw err; // unexpected — re-throw for caller to handle
  }
}

2.5 Parallel execution with async/await

// Sequential — slow (each waits for the previous)
async function sequential() {
  const users    = await fetchUsers();    // wait
  const products = await fetchProducts(); // wait
  const orders   = await fetchOrders();   // wait
  return { users, products, orders };     // total = sum of all times
}

// Parallel — fast (all start simultaneously)
async function parallel() {
  const [users, products, orders] = await Promise.all([
    fetchUsers(),
    fetchProducts(),
    fetchOrders()
  ]);
  return { users, products, orders }; // total = slowest of the three
}

// Conditional parallel — start some in parallel, await when needed
async function smart() {
  const userPromise    = fetchUser(1);    // started
  const settingsPromise = fetchSettings(); // started

  // Do some synchronous work while both are fetching
  const config = processLocalConfig();

  const user     = await userPromise;     // await both
  const settings = await settingsPromise;
  return { user, settings, config };
}

2.6 Async in loops

const ids = [1, 2, 3, 4, 5];

// WRONG — Promise.all ignores await inside forEach
async function wrong() {
  ids.forEach(async id => {
    const user = await fetchUser(id); // await inside forEach does nothing useful
    console.log(user);
  });
}

// Sequential — one at a time (use for...of)
async function sequential() {
  for (const id of ids) {
    const user = await fetchUser(id); // waits for each
    console.log(user.name);
  }
}

// Parallel — all at once
async function parallel() {
  const users = await Promise.all(ids.map(id => fetchUser(id)));
  users.forEach(u => console.log(u.name));
}

// Parallel with limit (process N at a time)
async function batched(ids, batchSize = 2) {
  for (let i = 0; i < ids.length; i += batchSize) {
    const batch = ids.slice(i, i + batchSize);
    const results = await Promise.all(batch.map(id => fetchUser(id)));
    results.forEach(u => console.log(u.name));
  }
}

2.7 Top-level await (modern)

// In ES2022+, await can be used at the top level of ES modules
// (no async wrapper needed in module files)

// data.js (an ES module)
const response = await fetch('/api/data.json');
export const data = await response.json();

// This works in modern browsers and Node.js 14+
// Not available in classic scripts — only in type="module" contexts

2.8 IIFE for async in non-module contexts

// Can't use top-level await in a regular script? Wrap in an async IIFE
(async () => {
  try {
    const data = await fetch('/api/data').then(r => r.json());
    console.log(data);
  } catch (err) {
    console.error(err);
  }
})();

3. Real World Example

// Authentication flow with async/await
async function authenticate(email, password) {
  const loginBtn = document.querySelector('#login-btn');
  const errorEl  = document.querySelector('#error-msg');

  loginBtn.disabled = true;
  loginBtn.textContent = 'Signing in...';
  errorEl.textContent = '';

  try {
    // Step 1: login
    const { token } = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    }).then(r => {
      if (!r.ok) throw new Error('Invalid credentials');
      return r.json();
    });

    // Step 2: get user profile using token
    const user = await fetch('/api/me', {
      headers: { 'Authorization': `Bearer ${token}` }
    }).then(r => r.json());

    // Step 3: parallel load of dashboard data
    const [notifications, settings] = await Promise.all([
      fetch('/api/notifications').then(r => r.json()),
      fetch('/api/settings').then(r => r.json())
    ]);

    sessionStorage.setItem('token', token);
    renderDashboard({ user, notifications, settings });

  } catch (err) {
    errorEl.textContent = err.message || 'Sign in failed. Please try again.';
  } finally {
    loginBtn.disabled = false;
    loginBtn.textContent = 'Sign In';
  }
}

4. Code Example

<input id="user-id" type="number" min="1" max="10" value="1">
<button id="load">Load User Data</button>
<div id="status"></div>
<div id="output"></div>

<script>
  // Simulated async APIs
  function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async function getUser(id) {
    await delay(300);
    if (id > 5) throw new Error(`No user with id ${id}`);
    return { id, name: `User ${id}`, email: `user${id}@example.com` };
  }

  async function getPosts(userId) {
    await delay(200);
    return [
      { id: 1, title: `Post A by user ${userId}` },
      { id: 2, title: `Post B by user ${userId}` }
    ];
  }

  const status = document.querySelector('#status');
  const output = document.querySelector('#output');

  document.querySelector('#load').addEventListener('click', async () => {
    const id = Number(document.querySelector('#user-id').value);
    status.textContent = 'Loading...';
    output.innerHTML   = '';

    try {
      // Parallel fetch — user and posts at the same time
      const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);

      status.textContent = `Loaded in ${Date.now()}ms`;

      const userDiv = document.createElement('div');
      userDiv.textContent = `${user.name} — ${user.email}`;

      const ul = document.createElement('ul');
      posts.forEach(post => {
        const li = document.createElement('li');
        li.textContent = post.title;
        ul.appendChild(li);
      });

      output.append(userDiv, ul);

    } catch (err) {
      status.textContent = '';
      const p = document.createElement('p');
      p.style.color = 'red';
      p.textContent = 'Error: ' + err.message;
      output.appendChild(p);
    }
  });
</script>

5. Code Breakdown

async event listener

The click handler is marked async. This allows using await inside it. Errors thrown inside an async event listener are caught by the try/catch — they don't go unhandled.

Promise.all with destructuring

Both getUser and getPosts start simultaneously. await Promise.all([...]) waits for both. Destructuring [user, posts] cleanly names each result.

Error thrown in getUser

When id > 5, getUser throws. This rejects the Promise returned by the async function. Promise.all then rejects, causing the outer await to throw, which the catch block catches.

6. Common Mistakes

Mistake 1 — Using await without async

// SyntaxError — await only valid inside async function
function load() {
  const data = await fetch('/api'); // SyntaxError
}

// Fix
async function load() {
  const data = await fetch('/api');
}

Mistake 2 — Sequential when parallel is better

// Slow — 3 × 500ms = 1500ms
async function slow() {
  const a = await fetchA(); // wait 500ms
  const b = await fetchB(); // wait 500ms
  const c = await fetchC(); // wait 500ms
  return { a, b, c };
}

// Fast — max(500, 500, 500) = 500ms
async function fast() {
  const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
  return { a, b, c };
}

Mistake 3 — Forgetting to await

async function save(data) {
  const result = saveToDb(data); // forgot await — result is a Promise, not data!
  console.log(result); // Promise { <pending> } — not useful
}

// Fix
async function save(data) {
  const result = await saveToDb(data);
  console.log(result); // actual data
}

Mistake 4 — await in forEach

// Bug — async forEach callbacks are not awaited
async function processAll(items) {
  items.forEach(async item => {
    await process(item); // this runs, but forEach doesn't wait for it
  });
  console.log('Done'); // logs before any items are processed!
}

// Fix — use for...of
async function processAll(items) {
  for (const item of items) {
    await process(item);
  }
  console.log('Done'); // logs after all processed
}

7. Best Practices

  1. Use async/await for sequential async operations — cleaner than .then() chains.
  2. Use Promise.all for parallel operations — don't await each one separately when they're independent.
  3. Always use try/catch with async/await — unhandled async errors can be invisible.
  4. Don't await in forEach — use for...of (sequential) or Promise.all + map (parallel).
  5. Re-throw errors you can't handle — don't silently swallow errors you don't know how to fix.
  6. Use finally for cleanup — hide spinners, re-enable buttons, regardless of success or failure.

8. Practice Exercise

  1. Convert this .then() chain to async/await: fetch('/api/users').then(r => r.json()).then(users => users.filter(u => u.active)).then(console.log).catch(console.error).
  2. Write an async function loadAll(ids) that fetches all users in parallel and returns the array of results.
  3. Write a retry(fn, attempts) function that calls an async function and retries up to N times on failure before throwing.

9. Assignment

Build a "GitHub Profile Viewer" (using the real GitHub API).

  1. Input: GitHub username. On submit, fetch https://api.github.com/users/{username} and display name, bio, followers, public repos, and avatar.
  2. Also fetch https://api.github.com/users/{username}/repos?sort=updated&per_page=5 in parallel and display the 5 most recently updated repos.
  3. Show a loading state during fetch, clear it when done.
  4. Handle errors: user not found (404), rate limit exceeded (403), network error.
  5. Use async/await throughout — no .then() chains.

Deliverable: One HTML file.

10. Interview Questions

  1. What does the async keyword do?
    It marks a function as asynchronous. The function always returns a Promise — if it returns a plain value, that value is wrapped in Promise.resolve(). It also enables the use of await inside the function.
  2. What does await do?
    It pauses execution of the async function until the Promise settles. If the Promise fulfills, await returns the value. If it rejects, await throws the error (catchable by try/catch). Other code continues running during the pause.
  3. What is the difference between sequential and parallel async/await?
    Sequential: await each one in turn — total time = sum of all durations. Parallel: start all Promises then await Promise.all — total time = slowest single operation. Always prefer parallel when operations are independent.
  4. How do you handle errors with async/await?
    Wrap await calls in try/catch blocks. The catch block receives the rejection reason as an Error object. Use finally for cleanup code that must run regardless of success or failure.

11. Additional Resources

  • MDN — async function
  • MDN — await
  • javascript.info — Async/await
  • MDN — Top-level await