Async/Await Architecture

Reviewed & published by Brayan K

async/await is JavaScript syntax that lets you write asynchronous, Promise-based code in a clean, sequential style, pausing on await until a Promise settles instead of chaining callbacks.

Part of the free JavaScript course at LearnCodingFast β€” hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

πŸ’‘ Running Code Locally: While this online editor runs real JavaScript, some advanced examples (like fetch to external APIs) may have limitations. For the best experience:

One thing to know for this lesson in particular: the built-in editor grabs your output at the moment your code stops running, and async results by definition arrive after that. So a snippet whose only output comes from a setTimeout or a .then() can look like it printed nothing here. Save it as demo.js and run node demo.js, or paste it into DevTools (F12), and you will see every line. The expected outputs written in the examples below are exactly what those two show.

Async/await is more than just a cleaner way to write Promises β€” it is the foundation of modern JavaScript architecture. Every large-scale platform in 2025 uses async/await as the core mechanism for handling network calls, database operations, streaming data, and server requests.

πŸ”₯ Why Async/Await Dominates Modern Development

Async/await is used everywhere:

// Simulated async function
async function getUserProfile(id) {
  // Simulating API calls
  const user = await Promise.resolve({ id, name: "John" });
  const stats = await Promise.resolve({ posts: 42, followers: 1000 });
  return { ...user, stats };
}

getUserProfile(1).then(console.log);

πŸ”₯ How Async Functions Actually Work Internally

When you write an async function, the engine transforms it into a Promise:

// This:
async function example() {
  return 42;
}

// Becomes this internally:
function example2() {
  return Promise.resolve(42);
}

example().then(console.log);
example2().then(console.log);

πŸ”₯ Sequential vs Parallel β€” The Biggest Beginner Mistake

// Simulated async tasks
const fetchA = () => new Promise(r => setTimeout(() => r("A"), 100));
const fetchB = () => new Promise(r => setTimeout(() => r("B"), 100));

// ❌ WRONG - Sequential (200ms total)
async function sequential() {
  console.time("sequential");
  const a = await fetchA();
  const b = await fetchB();
  console.timeEnd("sequential");
  console.log(a, b);
}

// βœ… CORRECT - Parallel (100ms total)
async function parallel() {
  console.time("parallel");
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  console.timeEnd("parallel");
  console.log(a, b);
}

sequential().then(() => parallel());

πŸ“– Worked example β€” watch the difference happen

Stopwatch numbers wobble from run to run, so this version prints a line when each fake API call starts and another when it finishes. Read those lines in order and the whole idea becomes visible: in the sequential run every call waits its turn, while in the parallel run all three start on the same tick. Give each call a different duration and one more thing shows up too β€” Promise.all hands results back in the order you listed them, not the order they finished in.

// WORKED EXAMPLE - the one async decision that actually changes speed:
// do these tasks depend on each other, or not?

// A fake API call. It announces when it starts and when it finishes,
// so you can SEE the difference instead of guessing at millisecond counts.
function fakeFetch(label, ms) {
  console.log("  start " + label);
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log("  done  " + label);
      resolve(label.toUpperCase());     // the "data" this call returns
    }, ms);
  });
}

// ---------- Sequential: each await waits for the previous one ----------
async function sequential() {
  console.log("SEQUENTIAL (one after another)");
  const profile = await fakeFetch("profile", 60);   // nothing else runs until this finishes
  const orders  = await fakeFetch("orders", 120);   // only NOW does this one start
  const offers  = await fakeFetch("offers", 30);
  console.log("  got: " + profile + " " + orders + " " + offers);
}

// ---------- Parallel: start all three, then wait for all three ----------
async function parallel() {
  console.log("PARALLEL (all at once)");
  const [profile, orders, offers] = await Promise.all([
    fakeFetch("profile", 60),    // calling the function STARTS the work
    fakeFetch("orders", 120),    // so all three timers are already running
    fakeFetch("offers", 30)
  ]);
  // Promise.all gives results back in ARRAY order, not finishing order:
  console.log("  got: " + profile + " " + orders + " " + offers);
}

// Run one, then the other, so the two sets of logs do not interleave.
sequential().then(parallel);

// βœ… Expected output (node demo.js, or your browser console):
// SEQUENTIAL (one after another)
//   start profile
//   done  profile
//   start orders
//   done  orders
//   start offers
//   done  offers
//   got: PROFILE ORDERS OFFERS
// PARALLEL (all at once)
//   start profile
//   start orders
//   start offers
//   done  offers
//   done  profile
//   done  orders
//   got: PROFILE ORDERS OFFERS
//
// Read the PARALLEL half twice: offers finishes FIRST (it is the 30ms one),
// yet it is still third in the results β€” Promise.all keeps the order you
// passed in, not the order they finished.

🎯 Your Turn β€” make three calls run at once

A blog post page needs its title, its author and its like count. None of them needs the others, so waiting for them one at a time is wasted time. Three blanks are marked with ___; everything else is written for you.

// 🎯 YOUR TURN - fill in the three blanks marked ___
// Three independent fake API calls (already written for you):
const fetchTitle  = () => new Promise(r => setTimeout(() => r("Async in 10 Minutes"), 80));
const fetchAuthor = () => new Promise(r => setTimeout(() => r("R. Hopper"), 40));
const fetchLikes  = () => new Promise(r => setTimeout(() => r(1284), 120));

async function loadPost() {
  // The three results are unpacked in the same order you list the calls.
  const [title, author, likes] = await Promise.___([   // πŸ‘‰ replace ___ with all
    fetchTitle(),
    fetchAuthor(),
    ___                                                // πŸ‘‰ add the third call: fetchLikes()
  ]);

  console.log(title + " by " + author + " - " + ___ + " likes");  // πŸ‘‰ replace ___ with likes
}

loadPost();

// βœ… Expected output once the blanks are filled:
// Async in 10 Minutes by R. Hopper - 1284 likes
//
// Hint: if you get "Promise.___ is not a function", you have not replaced blank 1.
// If you get "undefined likes", check blank 3 - it wants the variable, not a string.

πŸ”₯ Real-World Parallel Architecture

Imagine a backend for an app like YouTube:

// Simulated data fetchers
const getUser = id => Promise.resolve({ id, name: "User" });
const getSubscriptions = id => Promise.resolve(["Channel1", "Channel2"]);
const getRecommendations = id => Promise.resolve(["Video1", "Video2"]);
const getNotifications = id => Promise.resolve([{ msg: "New video" }]);
const getHistory = id => Promise.resolve(["Watched1", "Watched2"]);

// βœ… FAST - All requests in parallel
async function loadDashboard(id) {
  const [user, subs, recs, notes, history] = await Promise.all([
    getUser(id),
    getSubscriptions(id),
    getRecommendations(id),
    getNotifications(id),
    getHistory(id)
  ]);

  return { user, subs, recs, notes, history };
}

loadDashboard(1).then(data => console.log("Dashboard:", data));

πŸ”₯ Error Handling β€” Professional Pattern

// Professional error handling pattern
const wrap = async (promise) => {
  try {
    return [await promise, null];
  } catch (e) {
    return [null, e];
  }
};

// Usage
async function fetchData() {
  // Simulate sometimes failing
  if (Math.random() > 0.5) throw new Error("Network error");
  return { data: "Success!" };
}

async function main() {
  const [data, err] = await wrap(fetchData());

  if (err) {
    console.log("Error handled:", err.message);
    return { fallback: true };
  }
  
  console.log("Data received:", data);
}

main();

πŸ”₯ Avoiding the "Zombie Await" Anti-Pattern

const asyncTask = () => Promise.resolve("done");

// ❌ BAD - Sequential zombie awaits
async function bad() {
  console.time("bad");
  await asyncTask();
  await asyncTask();
  await asyncTask();
  console.timeEnd("bad");
}

// βœ… GOOD - Run in parallel
async function good() {
  console.time("good");
  await Promise.all([
    asyncTask(),
    asyncTask(),
    asyncTask()
  ]);
  console.timeEnd("good");
}

bad().then(() => good());

πŸ”₯ Designing Async Functions for Scalability

// ❌ Bad - depends on global state
let counter = 0;
async function badGetUser(id) {
  counter++;
  return { id, count: counter };
}

// βœ… Good - pure function, pass dependencies
async function goodGetUser(db, id) {
  // db would be passed in
  return { id, data: "from db" };
}

// Test - await only works INSIDE an async function, so wrap the calls in one
async function test() {
  console.log("Bad (impure):", await badGetUser(1));   // { id: 1, count: 1 }
  console.log("Bad (impure):", await badGetUser(1));   // { id: 1, count: 2 } - same input, different answer!
  console.log("Good (pure):", await goodGetUser({}, 1)); // { id: 1, data: 'from db' } every time
}
test();

πŸ”₯ The "Return Early" Pattern

async function purchase(user, item) {
  // Return early on validation failure
  if (user.balance < item.price) {
    return { ok: false, reason: "insufficient funds" };
  }

  // Simulate charging
  const receipt = await Promise.resolve({ 
    id: Date.now(), 
    item: item.name 
  });

  return { ok: true, receipt };
}

// Test cases
purchase({ balance: 10 }, { name: "Shirt", price: 25 })
  .then(r => console.log("Low balance:", r));

purchase({ balance: 100 }, { name: "Shirt", price: 25 })
  .then(r => console.log("Success:", r));

⚑ The Hidden Architecture of Async/Await

async function demo() {
  console.log("A");
  await Promise.resolve();
  console.log("B");
}

demo();
console.log("C");

// Output order explained:
// A - runs synchronously
// C - runs while awaiting
// B - runs after microtask

πŸ“Œ Error Surfacing: Before vs After Await

// Errors before await = sync rejection
async function beforeAwait() {
  throw new Error("sync fail");
  await Promise.resolve();
}

// Errors after await = async rejection
async function afterAwait() {
  await Promise.resolve();
  throw new Error("async fail");
}

// Safe fetch pattern
async function safeFetch(shouldFail) {
  try {
    if (shouldFail) throw new Error("Network error");
    return { data: "success" };
  } catch (err) {
    console.log("Caught:", err.message);
    return null; // graceful fallback
  }
}

safeFetch(true);
safeFetch(false).then(console.log);

πŸ—οΈ Architecting Async Pipelines

// Real-world async pipeline
async function processVideo(video) {
  console.log("Processing:", video);
  
  const raw = await Promise.resolve({ frames: 100 });

  // Parallel processing
  const [compressed, metadata] = await Promise.all([
    Promise.resolve({ size: "10MB" }),
    Promise.resolve({ duration: "5min" })
  ]);

  const thumbnail = await Promise.resolve({ url: "/thumb.jpg" });

  return {
    thumbnail,
    compressed,
    metadata
  };
}

processVideo("video.mp4").then(console.log);

⚑ Safe Timeout-Bound Operations

async function withTimeout(ms, promise) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timeout exceeded")), ms)
  );
  return Promise.race([promise, timeout]);
}

// Fast operation - succeeds
const fast = new Promise(r => setTimeout(() => r("Fast!"), 50));
withTimeout(100, fast)
  .then(r => console.log("Fast result:", r))
  .catch(e => console.log("Fast error:", e.message));

// Slow operation - times out
const slow = new Promise(r => setTimeout(() => r("Slow!"), 200));
withTimeout(100, slow)
  .then(r => console.log("Slow result:", r))
  .catch(e => console.log("Slow error:", e.message));

🎯 Mini-Challenge β€” Load an Account Page

Real pages are a mix: some data is independent, some data can only be fetched once you know the answer to an earlier call. This is the judgement call the whole lesson has been building towards, so this time there is no code to fill in β€” only an outline.

// 🎯 MINI-CHALLENGE: load an account page the right way
//
// The three fake API calls are written for you. You write loadAccountPage().
const getUser     = () => new Promise(r => setTimeout(() => r({ name: "Ada", planId: "pro" }), 60));
const getSettings = () => new Promise(r => setTimeout(() => r({ theme: "dark" }), 40));
const getPlan     = (planId) => new Promise(r => setTimeout(() => r({ name: planId.toUpperCase(), price: 12 }), 50));

async function loadAccountPage() {
  // 1. Start getUser() and getSettings() together with Promise.all and
  //    destructure the two results into  user  and  settings.
  // 2. THEN await getPlan(user.planId) - it depends on step 1, so it cannot
  //    go inside the same Promise.all.
  // 3. Print exactly three lines:
  //      "user: "  + user.name
  //      "theme: " + settings.theme
  //      "plan: "  + plan.name + " (" + plan.price + "/month)"
  // 4. Wrap steps 1-3 in one try/catch. In the catch, print
  //      "failed: " + err.message
  //
  // your code here
}

loadAccountPage();

// βœ… Expected output:
// user: Ada
// theme: dark
// plan: PRO (12/month)
//
// Check yourself: if you try to squeeze getPlan(user.planId) into the same
// Promise.all, you get "failed: Cannot access 'user' before initialization" -
// the engine telling you that call needs data which has not arrived yet.

🎯 Key Takeaways

When all these advanced techniques come togetherβ€”structured pipelines, concurrency control, safe resource handling, clear traces, timeouts, and robust error architectureβ€”you reach a level where async/await becomes a powerful tool rather than a confusing abstraction.

Practice quiz

An async function always returns what?

  • The raw value
  • undefined
  • A Promise
  • A generator

Answer: A Promise. async function example() { return 42; } is equivalent to returning Promise.resolve(42).

What does 'await' do inside an async function?

  • Pauses that function until the promise settles, without blocking the thread
  • Blocks the whole thread until the promise settles
  • Cancels the promise
  • Converts the promise to a callback

Answer: Pauses that function until the promise settles, without blocking the thread. await pauses the async function until the awaited promise settles, while the rest of the program keeps running.

Two independent 100ms tasks run with: await fetchA(); await fetchB(); How long does that take?

  • About 100ms
  • About 50ms
  • It runs instantly
  • About 200ms

Answer: About 200ms. Awaiting them one after another is sequential, so the times add up to roughly 200ms.

How do you run those two independent tasks in parallel (about 100ms total)?

  • await fetchA(); await fetchB();
  • const [a, b] = await Promise.all([fetchA(), fetchB()]);
  • Promise.race([fetchA(), fetchB()])
  • Run them in a for loop

Answer: const [a, b] = await Promise.all([fetchA(), fetchB()]);. Promise.all starts both at once and waits for both, so the total is about one task's duration.

What does this log in order: console.log('A'); await Promise.resolve(); console.log('B'); ... demo(); console.log('C');

  • A C B
  • A B C
  • C A B
  • A B then C

Answer: A C B. A runs synchronously, C runs while awaiting, and B runs after as a microtask: A, C, B.

In the tuple error-handling pattern, what does wrap(promise) resolve to on success?

  • [null, error]
  • Just data
  • [data, null]
  • A thrown error

Answer: [data, null]. wrap returns [await promise, null] on success and [null, e] on failure, so you destructure [data, err].

What is the 'zombie await' anti-pattern?

  • Forgetting to await a promise
  • Awaiting independent tasks sequentially when they could run in parallel
  • Using await outside async
  • Catching errors twice

Answer: Awaiting independent tasks sequentially when they could run in parallel. Sequentially awaiting independent operations wastes time; running them with Promise.all is faster.

What does the withTimeout(ms, promise) helper use to enforce the deadline?

  • Promise.all
  • setInterval
  • AbortController only
  • Promise.race against a timer that rejects

Answer: Promise.race against a timer that rejects. It races the real promise against a setTimeout-based promise that rejects, so the slower one loses.

An error thrown after an await in an async function surfaces as...

  • A synchronous throw
  • A rejected promise
  • A silent failure
  • A console warning

Answer: A rejected promise. Once execution has passed an await, a throw rejects the function's returned promise rather than throwing synchronously.

Why design async functions to be pure and take dependencies as parameters?

  • It makes them run faster
  • It is required by the syntax
  • It makes them scalable and testable rather than relying on global state
  • It avoids using await

Answer: It makes them scalable and testable rather than relying on global state. Passing dependencies in (instead of mutating globals) keeps async functions predictable and easy to scale and test.

Continue this course