Promises: States, Chaining & All Static Methods
Promise = future ka result. Pending → Fulfilled ya Rejected. Ek baar settle hua toh KABHI change nahi. .then() se chain, .catch() se errors, Promise.all se parallel.
A Promise represents the eventual result of an async operation. It has exactly 3 states: pending → fulfilled (with a value) OR pending → rejected (with a reason).
Once settled (fulfilled or rejected), a Promise NEVER changes state — this immutability is its core guarantee. A fulfilled promise stays fulfilled forever.
The new Promise((resolve, reject) => {}) executor runs synchronously, immediately. It is NOT deferred to the next tick.
resolve(value) transitions to fulfilled. reject(reason) transitions to rejected. You can only call them once — subsequent calls are silently ignored.
// Creating a Promise
const promise = new Promise((resolve, reject) => {
// Executor runs SYNCHRONOUSLY right now!
console.log("Executor runs immediately!");
const success = true;
if (success) {
resolve("It worked!"); // Promise becomes fulfilled
} else {
reject("It failed!"); // Promise becomes rejected
}
resolve("Second call"); // IGNORED! Can only settle once.
});
console.log("After promise creation");
// Output: "Executor runs immediately!" then "After promise creation"
// Throwing in executor = automatic rejection
const throwing = new Promise((resolve, reject) => {
throw new Error("Boom!"); // Automatically rejects the promise
// Equivalent to: reject(new Error("Boom!"))
});
Throwing inside the executor automatically rejects the promise. The throw is caught internally and converted to a rejection — no uncaught exception.
.then(onFulfilled, onRejected) returns a NEW Promise. This is the key to chaining — each handler produces a new promise that the next .then can wait on.
.catch(onRejected) is sugar for .then(null, onRejected). It catches ANY rejection above it in the chain.
.finally(onFinally) runs regardless of outcome. It receives NO arguments — it does not know if the chain succeeded or failed. It passes through the settled value.
Handler return rules: Return a value → next .then receives it. Return a promise → next .then waits for it. Throw → skips to .catch.
Forgetting to return in .then is the #1 Promise bug — the next handler receives undefined.
// Promise chaining — each .then returns a NEW promise
fetch("/api/user/1")
.then(res => res.json()) // Returns promise (waits for JSON parse)
.then(user => { // Receives parsed user object
console.log(user.name);
return fetch("/api/orders"); // MUST return! Or next .then gets undefined
})
.then(res => res.json()) // Returns promise
.then(orders => console.log(orders))
.catch(err => console.error(err)) // Catches ANY error in the ENTIRE chain
.finally(() => console.log("Done")); // Always runs, no arguments
// The #1 Promise bug: forgetting to return
fetch("/api/user/1")
.then(user => {
doSomething(user); // No return! Next .then gets undefined
})
.then(result => {
console.log(result); // undefined! Not the user!
});
// Fix: always return
fetch("/api/user/1")
.then(user => {
return doSomething(user); // Returns the value/promise
})
.then(result => {
console.log(result); // Correct value!
});
Promise.all([p1, p2, p3]): Waits for ALL to fulfill. Returns array of values in order. Fast-fails on the FIRST rejection — does not wait for others.
Promise.allSettled([p1, p2, p3]): Waits for ALL to settle. Returns array of status/value/reason objects. NEVER rejects — you always get all results.
Promise.race([p1, p2, p3]): Returns the FIRST settled promise (fulfill OR reject). Winner takes all.
Promise.any([p1, p2, p3]): Returns the FIRST FULFILLED promise. Ignores rejections. If ALL reject → throws AggregateError.
// Promise.all — parallel, fast-fail
const [user, orders] = await Promise.all([
fetch("/api/user/1").then(r => r.json()),
fetch("/api/orders").then(r => r.json())
]);
// Both requests run simultaneously! Total time = max(A, B), not A + B
// Promise.allSettled — never rejects, get all results
const results = await Promise.allSettled([
fetch("/api/a").then(r => r.json()), // Might fail
fetch("/api/b").then(r => r.json()), // Might fail
fetch("/api/c").then(r => r.json()) // Might fail
]);
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.log("Failed:", result.reason); // Still get the error
}
});
// Promise.race — timeout pattern
function fetchWithTimeout(url, ms) {
const fetchP = fetch(url);
const timeoutP = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
);
return Promise.race([fetchP, timeoutP]);
}
// Promise.any — first success
const source = await Promise.any([
fetch("https://primary-api.com/data").then(r => r.json()),
fetch("https://backup-api.com/data").then(r => r.json()),
fetch("https://fallback-api.com/data").then(r => r.json())
]);
// Returns data from whichever API responds successfully first
When to use what: Promise.all for independent parallel operations. Promise.allSettled for batch operations where you need all results. Promise.race for timeouts. Promise.any for fallback sources.
.then/.catch/.finally callbacks are queued as MICROTASKS, not macrotasks. This is a critical distinction.
THE RULE: After every macrotask → drain ALL microtasks → render (if needed) → next macrotask.
Microtasks always run before the next macrotask, even before rendering. This is guaranteed by the spec.
Promise.resolve().then(fn) runs BEFORE setTimeout(fn, 0) — microtask beats macrotask every time.
// Microtask vs Macrotask execution order
console.log("1 — sync");
setTimeout(() => console.log("2 — macrotask (setTimeout)"), 0);
Promise.resolve()
.then(() => console.log("3 — microtask (Promise.then)"))
.then(() => console.log("4 — microtask (chained Promise.then)"));
console.log("5 — sync");
// Output: 1, 5, 3, 4, 2
// Why?
// 1. Sync code runs first (1, 5)
// 2. Then ALL microtasks drain (3, 4)
// 3. Then macrotask runs (2)
// Microtasks can queue MORE microtasks — all drain before macrotask
Promise.resolve()
.then(() => {
console.log("A");
Promise.resolve().then(() => console.log("B")); // Queues another microtask
})
.then(() => console.log("C"));
setTimeout(() => console.log("D"), 0);
// Output: A, B, C, D
// A runs, queues B. C was already queued. B runs, then C. All microtasks done. Then D.
queueMicrotask(fn) is the modern way to schedule a microtask explicitly — use it when you need something to run after the current sync code but before any macrotask.
setTimeout(fn, 0) — it goes to the macrotask queue.Not every Promise pattern is a good one. Here are the most common mistakes:
Anti-pattern 1: Unnecessary wrapping — new Promise(resolve => resolve(fetch(url))) — just return fetch(url)! If an API already returns a Promise, wrapping it in new Promise is redundant and error-prone.
Anti-pattern 2: .then(f, errorHandler) vs .then(f).catch(errorHandler) — different behavior! The second catches errors in f too. The first only catches the original promise rejection.
Anti-pattern 3: Forgetting to return in .then — next handler gets undefined. This is the most common Promise bug.
Anti-pattern 4: Not handling rejections — unhandled promise rejection warning/error in Node.js and browsers.
// Anti-pattern 1: Unnecessary wrapping
function fetchData(url) {
return new Promise((resolve, reject) => {
fetch(url) // fetch ALREADY returns a Promise!
.then(res => resolve(res.json())) // Unnecessary wrapping!
.catch(err => reject(err));
});
}
// Fix: Just return the existing Promise!
function fetchData(url) {
return fetch(url).then(res => res.json()); // Clean!
}
// Anti-pattern 2: .then(f, errorHandler) misses errors in f
promise
.then(data => {
throw new Error("Oops!"); // This error is NOT caught by errorHandler!
}, err => {
// Only catches rejection of the ORIGINAL promise
});
// Fix: Use .then().catch() — catches errors in BOTH
promise
.then(data => {
throw new Error("Oops!"); // This IS caught by .catch!
})
.catch(err => {
console.error(err); // Catches errors from promise AND handler
});
// Anti-pattern 3: Not handling rejections
Promise.reject("unhandled"); // Warning: "Unhandled promise rejection"
// Always add .catch() or handle with try/catch in async/await
.catch(). Always return from .then(). Avoid new Promise when native APIs already return promises. Use .then().catch() instead of .then(null, handler).Lo kar liya — Key Points:
- ✅ A Promise has 3 states: pending, fulfilled, rejected — once settled, it never changes
- ✅ The executor function runs synchronously immediately; resolve/reject transition the promise
- ✅ .then() returns a NEW promise, enabling chains — forgetting to return is the #1 Promise bug
- ✅ .catch() catches any rejection above it in the chain; .finally() always runs regardless of outcome
- ✅ Promise.all waits for ALL to fulfill (fast-fails on first rejection); Promise.allSettled never rejects
- ✅ Promise.race returns the first settled; Promise.any returns the first fulfilled (AggregateError if all fail)
- ✅ .then callbacks are microtasks — they always run before setTimeout macrotasks
- ✅ Avoid unnecessary Promise wrapping, always return from .then, and use .catch() not .then(null, handler)
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