Async JavaScript — Callbacks, Promises & Await
Biryani order karo, table pe baitho — JS bhi waise kaam karta hai async mein.
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.
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.
function infinite() { infinite(); } → "Maximum call stack size exceeded". Always have a base case in recursion.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"
fs.readFile(path, (err, data) => { if (err) throw err; ... }). Forgetting to handle errors leads to silent failures.Promises were introduced in ES6 to solve callback hell. A Promise is an object representing the eventual completion or failure of an async operation.
• 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
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 promiseasync/await (ES2017) is syntactic sugar over Promises. It lets you write async code that looks synchronous — no more .then() chains.
1.
async keyword before a function makes it return a Promise automatically2.
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!
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."
• 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
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)
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 - ✅
asyncfunction ALWAYS returns a Promise, evenasync fn() { return 42; } - ✅ Microtask queue (Promises) ALWAYS empties before macrotask queue (setTimeout)
- ✅ Use
Promise.all()for parallel, sequentialawaitis slower
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login