Skip to main content
โณintermediate

Async / Await

async/await is syntactic sugar over Promises that makes asynchronous code look and feel synchronous. Write cleaner async code without .then() chains.

The Problem with .then() Chains

Promises are great, but deep chains can still get messy, especially with conditionals:

hljs javascript
// With promises
function getFullUserInfo(userId) {
  return fetchUser(userId)
    .then(user => {
      if (!user) throw new Error("User not found");
      return fetchProfile(user.id)
        .then(profile => {
          return fetchSettings(user.id)
            .then(settings => {
              return { user, profile, settings };
            });
        });
    });
}

Enter async/await

async/await lets you write the same logic in a way that reads like synchronous code:

hljs javascript
async function getFullUserInfo(userId) {
  const user = await fetchUser(userId);
  if (!user) throw new Error("User not found");

  const profile = await fetchProfile(user.id);
  const settings = await fetchSettings(user.id);

  return { user, profile, settings };
}

Much cleaner! Same power, better readability.

How It Works

  • async marks a function as asynchronous โ€” it always returns a Promise
  • await pauses execution of the async function until the Promise resolves
  • The await expression evaluates to the resolved value
hljs javascript
async function example() {
  console.log("1: before await");
  const result = await Promise.resolve(42);
  console.log("2: after await, result:", result);
  return "done";
}

example().then(val => console.log("3:", val));
console.log("4: this runs before the await resolves");

// Output order:
// 1: before await
// 4: this runs before the await resolves
// 2: after await, result: 42
// 3: done

await only pauses the async function โ€” the rest of the program keeps running.

Error Handling with try/catch

Use try/catch just like synchronous code:

hljs javascript
async function fetchUserData(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    return { user, posts };
  } catch (error) {
    console.error("Failed to fetch user data:", error.message);
    return null; // or re-throw: throw error;
  } finally {
    console.log("Fetch complete");
  }
}

โš ๏ธAlways handle errors in async functions

An unhandled rejection in an async function (a thrown error without a catch) will cause an UnhandledPromiseRejectionWarning. Either use try/catch or attach .catch() when calling the async function.

hljs javascript
// Option 1: try/catch inside
async function doWork() {
  try { await riskyOperation(); }
  catch (e) { /* handle */ }
}

// Option 2: .catch() on the call
doWork().catch(e => console.error(e));

Parallel vs Sequential

Understand the difference โ€” this is a common performance mistake:

hljs javascript
// โœ— Sequential โ€” each awaits the previous (300ms total)
async function sequential() {
  const a = await fetchA(); // 100ms
  const b = await fetchB(); // 100ms
  const c = await fetchC(); // 100ms
  return [a, b, c];
}

// โœ“ Parallel โ€” all start simultaneously (100ms total)
async function parallel() {
  const [a, b, c] = await Promise.all([
    fetchA(), // starts immediately
    fetchB(), // starts immediately
    fetchC(), // starts immediately
  ]);
  return [a, b, c];
}

Only use sequential await when each operation depends on the previous result:

hljs javascript
// Sequential is correct here โ€” each step needs the previous result
async function process() {
  const order = await createOrder();       // needs to exist first
  const payment = await processPayment(order.id); // needs order
  const receipt = await sendReceipt(payment.id);  // needs payment
  return receipt;
}

await with Promise.all

Combine await with Promise.all for clean parallel code:

hljs javascript
async function loadDashboard(userId) {
  // All three start simultaneously
  const [user, notifications, stats] = await Promise.all([
    getUser(userId),
    getNotifications(userId),
    getStats(userId),
  ]);

  return { user, notifications, stats };
}

Top-Level await (ES2022)

In ES modules, you can use await at the top level without wrapping in an async function:

hljs javascript
// In a .mjs file or module:
const data = await fetch("/api/data").then(r => r.json());
console.log(data);

for await...of โ€” Async Iteration

Iterate over async iterables or arrays of Promises:

hljs javascript
async function processItems(ids) {
  for await (const id of ids) {
    const data = await fetch(`/api/items/${id}`);
    console.log(await data.json());
  }
}

// Or with an async generator
async function* fetchPages(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    yield await response.json();
  }
}

Async Functions Always Return a Promise

Even if you return a plain value:

hljs javascript
async function getNumber() {
  return 42;
}

// Returns a Promise, not 42
const result = getNumber();
console.log(result); // Promise { <fulfilled>: 42 }

// To get the value:
result.then(v => console.log(v)); // 42

// Or inside another async function:
async function main() {
  const num = await getNumber();
  console.log(num); // 42
}

Practical Patterns

Retry with backoff

hljs javascript
async function fetchWithRetry(url, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (attempt === maxRetries) throw error;
      const wait = 2 ** attempt * 100; // exponential backoff
      console.log(`Attempt ${attempt} failed. Retrying in ${wait}ms...`);
      await new Promise(resolve => setTimeout(resolve, wait));
    }
  }
}

Timeout

hljs javascript
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

async function main() {
  try {
    const data = await withTimeout(fetch("/api/slow"), 5000);
    console.log(data);
  } catch (e) {
    console.error(e.message);
  }
}
โ–ถTry it yourself

Key Takeaways

  • async makes a function return a Promise; await waits for a Promise to resolve
  • Use try/catch/finally for error handling โ€” much cleaner than .catch() chains
  • Independent operations should run in parallel with Promise.all โ€” sequential await is a performance trap
  • await only pauses the current async function, not the whole program
  • Async functions always return Promises, even when returning plain values

Ready to test your knowledge?

Take a quiz on what you just learned.

Take the Quiz โ†’