🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
JS MASTER CLASS /// MASTER THE ENGINE /// BUILD LOGIC /// ASYNC PATTERNS /// JS MASTER CLASS /// MASTER THE ENGINE ///

JavaScript Async/Await: Write Asynchronous Code That Reads Like Synchronous - In-Depth Guide

Master JavaScript async/await: the async keyword, await pause-and-unwrap, try/catch error handling, serial vs parallel execution, Promise.all/allSettled/race/any, the loading state pattern, and top-level await.

Total XP: 0|💻 javascript XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

What is the primary advantage discussed here?


🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Async/await is syntactic sugar over Promises that lets asynchronous code read like ordinary synchronous code. This lesson covers the async and await keywords, try/catch error handling, the difference between serial and parallel execution, the four Promise combinators, and top-level await.

1JavaScript Async/Await Part 1

Async/Await is syntactic sugar built on Promises. Instead of chaining .then() callbacks, you write flat, linear code that LOOKS synchronous — but runs asynchronously without blocking the browser.

+
// ❌ Promise chain — nested, hard to follow
fetch('/api/user')
  .then(res => res.json())
  .then(user => {
    return fetch(`/api/posts/${user.id}`);
  })
  .then(res => res.json())
  .then(posts => console.log(posts))
  .catch(err => console.error(err));

// ✅ Async/Await — flat, linear, readable
async function loadPosts() {
  const res = await fetch('/api/user');
  const user = await res.json();
  const postsRes = await fetch(`/api/posts/${user.id}`);
  const posts = await postsRes.json();
  console.log(posts);
}
localhost:3000

Flat is Better

.then()
 .then()
  .then()
await
await
await

2JavaScript Async/Await Part 2

The 'async' keyword marks a function as asynchronous. The critical rule: an async function ALWAYS returns a Promise — even if you return a plain value like a string or number.

+
// async function ALWAYS returns a Promise
async function getStatus() {
  return 'Online'; // ← returns Promise<'Online'>
}

// Proof: .then() works on the result
getStatus().then(val => {
  console.log(val);         // 'Online'
  console.log(typeof val);  // 'string'
});

// Equivalent without async:
function getStatusManual() {
  return Promise.resolve('Online');
}
localhost:3000

Always a Promise

return 'Online'
⬇️
Promise.resolve('Online')

3JavaScript Async/Await Part 3

The 'await' keyword pauses execution of the async function until the Promise resolves. It UNWRAPS the Promise, giving you the resolved value directly — no .then() needed. The rest of your app keeps running while this function waits.

+
async function showUser() {
  console.log('1. Start');         // runs immediately

  const user = await fetchUser();  // ⏸ PAUSES here
  // ↑ fetchUser() returns a Promise
  // ↑ await unwraps it → user = resolved value

  console.log('2. User:', user);   // runs AFTER resolve
  console.log('3. Done');          // runs AFTER line above
}

showUser();
console.log('4. Non-blocking!');   // runs IMMEDIATELY
localhost:3000

Timeline

1. showUser() starts
2. await fetchUser() ⏸️
3. Main app continues...
4. Promise resolves ✅
5. showUser() finishes

4JavaScript Async/Await Part 4

Error handling with async/await uses the same try...catch blocks you already know from synchronous code. If ANY awaited Promise rejects, execution jumps to the catch block — network errors, API errors, JSON parse errors, all caught in one place.

+
async function loadUser(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:', user.name);
    return user;

  } catch (error) {
    console.error('Failed to load user:', error.message);
    // Network error, HTTP error, or JSON parse error
    // ALL caught here
  } finally {
    console.log('Request completed');
  }
}
localhost:3000

Unified Catch

🌐 Network Error
🚫 HTTP 404
🐛 JSON Parse
⬇️
catch (error)

5JavaScript Async/Await Part 5

DANGER: awaiting one after another is SERIAL — each waits for the previous to finish. If tasks are independent, this wastes time. Two 2-second requests take 4 seconds serial, but only 2 seconds parallel.

+
// ❌ SERIAL — total time: ~4 seconds
async function loadSerial() {
  const users = await fetchUsers();  // 2s ⏳
  const posts = await fetchPosts();  // 2s ⏳ (waits for users!)
  // Total: 2s + 2s = 4s
  return { users, posts };
}

// Timeline:
// |--users (2s)--|--posts (2s)--|
// 0s             2s             4s
localhost:3000

Serial Bottleneck

users (2s)
posts (2s)
Total: 4s ⏳

6JavaScript Async/Await Part 6

Promise.all() fires all Promises at once and waits for ALL to resolve. Independent tasks run in parallel — two 2-second requests complete in just 2 seconds total. But if ANY one fails, the entire Promise.all rejects.

+
// ✅ PARALLEL — total time: ~2 seconds
async function loadParallel() {
  const [users, posts] = await Promise.all([
    fetchUsers(),  // 2s ⏳ starts immediately
    fetchPosts(),  // 2s ⏳ starts immediately
  ]);
  // Total: max(2s, 2s) = 2s ← 50% faster!
  return { users, posts };
}

// Timeline:
// |--users (2s)--|
// |--posts (2s)--|
// 0s             2s  ← both done!
localhost:3000

Parallel Execution

users (2s)
posts (2s)
Total: 2s ⚡

7JavaScript Async/Await Part 7

Beyond Promise.all: allSettled() waits for ALL regardless of failures. race() resolves/rejects with the FIRST to finish. any() resolves with the FIRST success (ignores rejections). Each solves a different concurrency pattern.

+
const promises = [
  fetch('/api/fast'),   // resolves in 1s
  fetch('/api/slow'),   // resolves in 3s
  fetch('/api/broken'), // rejects in 2s
];

// Promise.all — FAIL-FAST (one reject = all reject)
await Promise.all(promises); // ❌ rejects at 2s

// Promise.allSettled — WAIT FOR ALL
await Promise.allSettled(promises);
// [{status:'fulfilled'}, {status:'fulfilled'}, {status:'rejected'}]

// Promise.race — FIRST TO FINISH (success or failure)
await Promise.race(promises); // resolves at 1s (fast wins)

// Promise.any — FIRST SUCCESS (ignores rejections)
await Promise.any(promises); // resolves at 1s (fast wins)
localhost:3000

Promise Combinators

all()
All or nothing. Fail-fast.
allSettled()
Wait for all. Never rejects.
race()
First to finish wins (success/fail).
any()
First to SUCCEED wins.

8JavaScript Async/Await Part 8

Real-world pattern: manage loading, data, and error states with async/await. This is the exact pattern used in React, Vue, and every modern framework for data fetching.

+
async function loadDashboard() {
  let loading = true;
  let data = null;
  let error = null;

  try {
    const [users, stats] = await Promise.all([
      fetchUsers(),
      fetchStats(),
    ]);
    data = { users, stats };
  } catch (err) {
    error = err.message;
  } finally {
    loading = false;
  }

  // State: { loading: false, data: {...}, error: null }
  return { loading, data, error };
}
localhost:3000

State Management

Loading:true → false
Data:null → {...}
Error:null → string

9JavaScript Async/Await Part 9

Modern JavaScript (ES modules) supports top-level await — you can use await outside any function at the module's root. No async wrapper needed. This is the standard in Node.js ESM and modern browsers.

+
// ── In an ES Module (.mjs or type="module") ──

// ✅ Top-level await — no async wrapper needed
const config = await fetch('/api/config').then(r => r.json());
console.log('App config loaded:', config.appName);

// ✅ Dynamic imports with await
const { default: Chart } = await import('./chart.js');
const chart = new Chart('#canvas');
localhost:3000

Top-Level Await

(async () => { await... })()
await... (in ES Modules)

10JavaScript Async/Await Part 10

Async/Await mastered: async wraps returns in Promises, await pauses and unwraps, try/catch handles all errors, Promise.all for parallel execution, and allSettled/race/any for advanced patterns. Next: the Fetch API.

+
localhost:3000

Async Mastered

11Step-by-Step Breakdown

Async/Await is syntactic sugar built on Promises. Instead of chaining .then() callbacks, you write flat, linear code that LOOKS synchronous — but runs asynchronously without blocking the browser.

The 'async' keyword marks a function as asynchronous. The critical rule: an async function ALWAYS returns a Promise — even if you return a plain value like a string or number.

The 'await' keyword pauses execution of the async function until the Promise resolves. It UNWRAPS the Promise, giving you the resolved value directly — no .then() needed. The rest of your app keeps running while this function waits.

Checkpoint: Can you use the 'await' keyword inside a regular function that is NOT marked with 'async'?

  • Yes, it works everywhere
  • No, 'await' requires an 'async' function

Error handling with async/await uses the same try...catch blocks you already know from synchronous code. If ANY awaited Promise rejects, execution jumps to the catch block — network errors, API errors, JSON parse errors, all caught in one place.

DANGER: awaiting one after another is SERIAL — each waits for the previous to finish. If tasks are independent, this wastes time. Two 2-second requests take 4 seconds serial, but only 2 seconds parallel.

Promise.all() fires all Promises at once and waits for ALL to resolve. Independent tasks run in parallel — two 2-second requests complete in just 2 seconds total. But if ANY one fails, the entire Promise.all rejects.

Checkpoint: If one promise inside Promise.all fails, what happens to the entire operation?

  • It continues with the successful ones
  • It immediately rejects the whole Promise.all

Beyond Promise.all: allSettled() waits for ALL regardless of failures. race() resolves/rejects with the FIRST to finish. any() resolves with the FIRST success (ignores rejections). Each solves a different concurrency pattern.

Real-world pattern: manage loading, data, and error states with async/await. This is the exact pattern used in React, Vue, and every modern framework for data fetching.

Checkpoint: How do you capture the value returned from an awaited Promise?

  • Using a .then() callback
  • Assigning it to a const/let variable

Modern JavaScript (ES modules) supports top-level await — you can use await outside any function at the module's root. No async wrapper needed. This is the standard in Node.js ESM and modern browsers.

Async/Await mastered: async wraps returns in Promises, await pauses and unwraps, try/catch handles all errors, Promise.all for parallel execution, and allSettled/race/any for advanced patterns. Next: the Fetch API.

Level Up 🚀

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

Fully supported.

Accessibility (A11y)

1Announce Loading and Error States from Async Operations to Screen Readers

Fetching data with async/await often changes the UI between loading, loaded, and error states, but a purely visual spinner or error message is invisible to screen reader users. Wrap the status region in an `aria-live="polite"` container so state changes ('Loading...', 'Failed to load data') are announced automatically.

SEO Implications

  • 1

    Awaited Data That Renders After Initial Load Can Be Invisible to Crawlers

    If a page's main content depends on an async/await fetch that resolves only after the initial HTML is delivered, crawlers that don't fully execute and wait on JavaScript may index a page with missing content. Server-side rendering or static generation that awaits the data before responding avoids this gap.

Best Practices

Use Promise.all() for Independent Async Operations Instead of Sequential Awaits

Awaiting one request, then another, forces the second to wait even though it doesn't depend on the first's result — this needlessly adds their durations together. Fire independent requests concurrently and await them together with Promise.all() so the total time is the duration of the slowest one.

Always Wrap Await Calls in Try/Catch (or Handle Rejections Explicitly)

An awaited Promise that rejects without a surrounding try/catch throws inside the async function and, if uncaught anywhere in the call chain, can crash a Node process or surface as an unhandled promise rejection in the browser. Every await that can fail should be inside a try/catch or have its rejection handled by the caller.

Frequent Bugs

THE BUG

Awaiting independent async calls one after another, unintentionally serializing requests that could run in parallel.

THE FIX

`const a = await fetchA(); const b = await fetchB();` makes fetchB wait for fetchA to finish even if they don't depend on each other. Start both first (`const pA = fetchA(); const pB = fetchB();`) and await them together with `Promise.all([pA, pB])` to run them concurrently.

THE BUG

Using Promise.all() when partial failures should be tolerated, and losing all successful results because one request rejected.

THE FIX

Promise.all() rejects entirely the instant any one of its promises rejects, discarding every other result even if they succeeded. Use Promise.allSettled() when you need to know the outcome of every request regardless of individual failures.

Real-World Examples

Loading a Dashboard's Data with Promise.all and a Loading/Error State

A dashboard needed to fetch a user's profile and their usage stats concurrently, show a loading indicator while both were in flight, and display a clear error message if either request failed.

async function loadDashboard() {
  let loading = true, data = null, error = null;
  try {
    const [users, stats] = await Promise.all([fetchUsers(), fetchStats()]);
    data = { users, stats };
  } catch (err) {
    error = err.message;
  } finally {
    loading = false;
  }
  return { loading, data, error };
}

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Mutating arrays while iterating over them

// Wrong items.forEach((item, index) => { if (item === 'remove') items.splice(index, 1); }); // Correct const newItems = items.filter(item => item !== 'remove');

The Solution //

Modifying an array's length or contents while looping through it (with a for loop or forEach) can cause elements to be skipped. Use methods like filter() or map() instead.

The Error //

Forgetting to await asynchronous functions

// Wrong const data = fetch('api/data'); console.log(data.json()); // Error // Correct const response = await fetch('api/data'); const data = await response.json();

The Solution //

If a function returns a Promise, you must use 'await' (or .then) to get its resolved value. Otherwise, your variable will hold a Promise object instead of the data.

Lesson Glossary

[01]async

A keyword that marks a function as asynchronous. An async function always returns a Promise — even if you return a plain value, it is wrapped in Promise.resolve().

Code Preview
async function load() { return 'data'; }

[02]await

A keyword that pauses an async function's execution until a Promise resolves, then unwraps and returns the resolved value directly. Can only be used inside async functions or ES module top-level.

Code Preview
const data = await fetchData();

[03]try...catch

A control structure for error handling. In async/await, if any awaited Promise rejects, execution jumps to the catch block. The finally block runs regardless.

Code Preview
try { await op(); } catch (e) { ... }

[04]finally

A block that executes after try/catch regardless of success or failure. Perfect for cleanup: hiding spinners, closing connections, resetting state.

Code Preview
try { ... } catch { ... } finally { cleanup(); }

[05]Promise.all()

Takes an array of Promises and returns a single Promise that resolves when ALL resolve. Fail-fast: rejects immediately if ANY promise rejects.

Code Preview
const [a, b] = await Promise.all([p1, p2]);

[06]Promise.allSettled()

Waits for ALL Promises to complete regardless of success/failure. Returns an array of {status, value/reason} objects. Never rejects on its own.

Code Preview
const results = await Promise.allSettled([p1, p2]);

[07]Promise.race()

Resolves or rejects with the FIRST Promise to settle (whether fulfilled or rejected). Useful for implementing timeouts.

Code Preview
const first = await Promise.race([fetch, timeout]);

[08]Promise.any()

Resolves with the FIRST Promise to fulfill successfully, ignoring rejections. Throws AggregateError only if ALL promises reject.

Code Preview
const fastest = await Promise.any([p1, p2, p3]);

[09]Serial Execution

Awaiting tasks one after another — each waits for the previous to finish. Total time = sum of all tasks. Use for dependent operations.

Code Preview
const a = await t1(); const b = await t2(a);

[10]Parallel Execution

Firing all independent Promises simultaneously with Promise.all. Total time = max(individual times). Much faster for independent tasks.

Code Preview
const [a, b] = await Promise.all([t1(), t2()]);

[11]Top-Level Await

Using await outside any function at the root of an ES module. Supported in modern browsers and Node.js 14.8+. No async wrapper needed.

Code Preview
const cfg = await fetch('/config').then(r => r.json());

[12]Syntactic Sugar

Syntax designed to make code easier to read while the underlying behavior uses existing features. Async/await is syntactic sugar over Promises.

Code Preview
async/await → Promise.then() under the hood

Continue Learning