Chapter 1.5☕ 16 min read

Async JavaScript — Callbacks, Promises & Await

Biryani order karo, table pe baitho — JS bhi waise kaam karta hai async mein.

01Call Stack & Blocking

JavaScript is single-threaded — it has exactly one call stack. It can only do one thing at a time. Think of a restaurant with one waiter: they take an order, go to the kitchen, come back, serve — one task at a time.

The Call Stack is a LIFO (Last In, First Out) data structure. When you call a function, it gets pushed onto the stack. When it returns, it gets popped off. The engine always executes whatever is on top of the stack.
function greet(name) {
  console.log("Hello " + name);
}

function main() {
  greet("Sai");
  console.log("Done");
}

main();

// Call stack lifecycle:
// 1. main() pushed
// 2.   greet("Sai") pushed
// 3.     console.log() pushed → executes → popped
// 4.   greet() popped
// 5.   console.log("Done") pushed → executes → popped
// 6. main() popped → stack empty

The blocking problem: If a function takes 5 seconds, the entire page freezes. No clicks, no scrolling, nothing. This is why we need async — to hand off slow work without blocking the stack.

Stack Overflow: If recursion never stops, frames keep piling up until the stack runs out of memory. function infinite() { infinite(); } → "Maximum call stack size exceeded". Always have a base case in recursion.
02Callbacks & Callback Hell

The original solution to the blocking problem: callbacks. A callback is simply a function you pass to another function to be called later.

// setTimeout hands off work to the browser's Web APIs
// The callback runs AFTER the timer — it doesn't block!
console.log("1 - Order placed");

setTimeout(function() {
  console.log("2 - Biryani served!");
}, 3000);

console.log("3 - Chatting with friend");

// Output: 1, 3, 2 — notice 3 prints BEFORE 2!

Callbacks are everywhere: setTimeout, setInterval, event listeners, Node.js fs.readFile(path, callback).

The callback hell problem: When you need multiple async operations in sequence:

getUser(id, function(user) {
  getPosts(user.id, function(posts) {
    getComments(posts[0].id, function(comments) {
      getAuthor(comments[0].authorId, function(author) {
        // 4 levels deep — hard to read, hard to debug
        console.log(author.name);
      });
    });
  });
});
// ↑ This pyramid shape is called "Callback Hell"
Error handling in callbacks: Node.js uses the "error-first callback" pattern — the first argument is always an error (null if no error). fs.readFile(path, (err, data) => { if (err) throw err; ... }). Forgetting to handle errors leads to silent failures.
03Promises — The Fix

Promises were introduced in ES6 to solve callback hell. A Promise is an object representing the eventual completion or failure of an async operation.

Three states — and they're one-way:
Pending → initial state, neither fulfilled nor rejected
Fulfilled → operation completed successfully (calls .then())
Rejected → operation failed (calls .catch())

Once settled (fulfilled or rejected), a promise never changes state again. This is the guarantee.
// Creating a promise
const orderBiryani = new Promise((resolve, reject) => {
  const ready = true;
  setTimeout(() => {
    if (ready) {
      resolve("🎉 Hyderabadi Biryani!");  // fulfilled
    } else {
      reject("❌ Biryani nahi bana");     // rejected
    }
  }, 2000);
});

// Consuming a promise
orderBiryani
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log("Order process complete"));

The magic of chaining: .then() always returns a new promise. This is what makes flattening possible:

// Same logic as callback hell — but FLAT
getUser(id)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => getAuthor(comments[0].authorId))
  .then(author => console.log(author.name))
  .catch(err => console.error("Any step failed:", err));

// One .catch() handles errors from ANY step in the chain
📋 Golden Rule: Always return inside .then() if you want to chain the next operation. Forgetting return means the next .then() gets undefined instead of the promise result.
// Static promise methods
Promise.all([p1, p2, p3])       // ALL must resolve — rejects on FIRST failure
Promise.allSettled([p1, p2, p3]) // waits for ALL — returns {status, value/reason} for each
Promise.race([p1, p2, p3])       // resolves/rejects with FIRST to settle
Promise.resolve(42)               // immediately fulfilled promise
Promise.reject(new Error("fail")) // immediately rejected promise
04async/await — Clean Async

async/await (ES2017) is syntactic sugar over Promises. It lets you write async code that looks synchronous — no more .then() chains.

Two rules to remember:
1. async keyword before a function makes it return a Promise automatically
2. await can ONLY be used inside an async function (or ES module top-level)

async function f() { return 42; } → returns Promise<42>, not 42. This is the #1 interview trap.
async function getOrder() {
  try {
    const user = await getUser(id);        // pauses here, doesn't block
    const posts = await getPosts(user.id); // resumes when promise resolves
    const comments = await getComments(posts[0].id);
    const author = await getAuthor(comments[0].authorId);
    console.log(author.name);
  } catch (err) {
    console.error("Something failed:", err);
  }
}

// Compare to the .then() chain — same result, much cleaner!

Sequential vs Parallel — this is critical:

// SEQUENTIAL — each waits for the previous (slow!)
const user = await getUser(1);      // 1 second
const posts = await getPosts(1);    // 1 second  → total: 2 seconds

// PARALLEL — both run at the same time (fast!)
const [user, posts] = await Promise.all([
  getUser(1),    // 1 second (running simultaneously)
  getPosts(1)    // 1 second
]);                              // total: 1 second!
await doesn't block the thread! It only pauses the execution inside that async function. The call stack is freed — other code can run, the UI stays responsive. This is fundamentally different from a synchronous blocking call. Under the hood, it's still promises and microtasks.
05Event Loop — Microtask vs Macrotask

The Event Loop is the mechanism that makes async work in JavaScript. It continuously checks: "Is the call stack empty? If yes, take the next item from a queue."

Two queues with different priorities:
Microtask Queue (higher priority): Promise .then/.catch, queueMicrotask(), MutationObserver
Macrotask Queue (lower priority): setTimeout, setInterval, I/O operations, UI rendering

The rule: The event loop empties the ENTIRE microtask queue before taking even ONE item from the macrotask queue. After each macrotask, it checks the microtask queue again.
console.log("1 - Sync");

setTimeout(() => {
  console.log("2 - Macrotask (setTimeout)");
}, 0);

Promise.resolve().then(() => {
  console.log("3 - Microtask (Promise)");
});

console.log("4 - Sync");

// OUTPUT ORDER:
// 1 - Sync            ← call stack, runs immediately
// 4 - Sync            ← call stack, runs immediately
// 3 - Microtask       ← ENTIRE microtask queue empties first
// 2 - Macrotask       ← then ONE macrotask runs
📋 The Execution Order (memorize this!):
1. All synchronous code (call stack)
2. ENTIRE microtask queue (all Promise .then callbacks)
3. ONE macrotask (one setTimeout callback)
4. Check microtask queue again
5. Next macrotask... and so on

Promise always beats setTimeout — even with setTimeout(fn, 0)!
// Tricky variation — microtasks generated during microtasks
Promise.resolve().then(() => {
  console.log("A");
  Promise.resolve().then(() => console.log("B"));
});
Promise.resolve().then(() => console.log("C"));

// Output: A, C, B
// A runs → creates new microtask (B)
// C runs (was already queued before B was created)
// B runs (microtask queue fully drained)
Why this matters: This ordering is THE most asked JavaScript interview topic. If you understand that microtasks always drain before macrotasks, you can solve any "predict the output" question. In real code, it means your Promise handlers always run before the next setTimeout — useful for timing-sensitive logic.

Lo kar liya — Key Points:

  • ✅ JavaScript is single-threaded — one call stack, LIFO order
  • ✅ Callbacks solve blocking but create "callback hell" pyramid
  • ✅ Promises have 3 states: pending → fulfilled/rejected (one-way, never reverses)
  • .then() always returns a new Promise — that's how chaining works
  • async function ALWAYS returns a Promise, even async fn() { return 42; }
  • ✅ Microtask queue (Promises) ALWAYS empties before macrotask queue (setTimeout)
  • ✅ Use Promise.all() for parallel, sequential await is slower
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase