Promises
A Promise is an object representing the eventual result of an asynchronous operation. It exists in exactly one of three states:
🎯 What you'll learn
- What a Promise Is
- Creating a Promise
- Consuming a Promise
.then()Always Returns a NEW Promise- Static Helpers:
Promise.resolve/Promise.reject
1What a Promise Is
A Promise is an object representing the eventual result of an asynchronous operation. It exists in exactly one of three states:
| State | Meaning | Can transition to |
|---|---|---|
| pending | operation not finished yet | fulfilled or rejected |
| fulfilled | completed successfully, has a value | (final — stays fulfilled forever) |
| rejected | failed, has a reason (usually an Error) | (final — stays rejected forever) |
Once a Promise settles (fulfilled or rejected), it never changes state again — this is what makes Promises reliable to reason about, unlike raw callbacks which could theoretically be called multiple times.
2Creating a Promise
const promise = new Promise((resolve, reject) => {
const success = Math.random() > 0.3;
setTimeout(() => {
if (success) resolve("data loaded");
else reject(new Error("failed to load"));
}, 100);
});The executor function (passed to new Promise) runs immediately and synchronously, but resolve/reject are typically called later, once the async work completes.
3Consuming a Promise
promise
.then(result => console.log("success:", result))
.catch(error => console.log("failure:", error.message))
.finally(() => console.log("always runs, success or failure"));.then(onFulfilled, onRejected)— handles both outcomes; the second argument is optional..catch(onRejected)— shorthand for.then(undefined, onRejected)..finally(fn)— runs regardless of outcome, receives no argument, cannot change the result.
4.then() Always Returns a NEW Promise
const p1 = Promise.resolve(5);
const p2 = p1.then(n => n * 2);
console.log(p1 === p2); // false — .then() creates a brand-new PromiseThis is the mechanism that makes chaining possible (full detail in the Promise Chaining topic) — each .then() returns a new Promise resolved with whatever its callback returns.
5Static Helpers: Promise.resolve / Promise.reject
Promise.resolve(42); // an already-fulfilled Promise, value 42
Promise.reject(new Error("bad")); // an already-rejected Promise
// Useful for normalizing values that MIGHT already be Promises:
function ensurePromise(value) {
return Promise.resolve(value); // wraps a plain value; if `value` is already a Promise, returns it as-is
}6Errors Thrown Inside .then() Become Rejections
Promise.resolve(1)
.then(() => { throw new Error("oops"); })
.catch(e => console.log("caught:", e.message)); // "caught: oops"A thrown error inside any .then() callback is automatically converted into a rejected Promise, which the next .catch() (or rejection handler) picks up — no manual reject() call needed inside .then().
7A Promise's Callback Always Runs Asynchronously
console.log("1");
Promise.resolve().then(() => console.log("3"));
console.log("2");
// 1, 2, 3 — .then() callbacks NEVER run synchronously, even for an already-resolved PromiseThis connects directly to the Microtasks vs Macrotasks topic — .then() callbacks are always scheduled as microtasks, never invoked immediately inline.
Under the HoodThe Internal Slots Behind "Pending/Fulfilled/Rejected"▾
The spec defines a Promise as an object with several internal slots — not regular properties you can access, but real state the engine tracks per Promise instance. Understanding them explains exactly why .then() behaves the way it does.
| Internal slot | Holds |
|---|---|
[[PromiseState]] | "pending", "fulfilled", or "rejected" |
[[PromiseResult]] | the fulfillment value or rejection reason, once settled |
[[PromiseFulfillReactions]] | a LIST of pending success-handler callbacks, waiting for fulfillment |
[[PromiseRejectReactions]] | a LIST of pending failure-handler callbacks, waiting for rejection |
What .then() actually does to these slots
const p = new Promise(resolve => setTimeout(() => resolve(42), 1000));
p.then(value => console.log(value)); // called while p is still PENDINGCalling .then() on a still-pending Promise doesn't run anything yet — it appends a reaction record (your callback, plus the new Promise .then() itself returns) onto [[PromiseFulfillReactions]] and [[PromiseRejectReactions]]. Nothing executes until the Promise actually settles.
What resolve()/reject() actually do
When resolve(value) finally runs, the engine: sets [[PromiseState]] to "fulfilled", sets [[PromiseResult]] to value, then walks the entire [[PromiseFulfillReactions]] list and schedules one microtask per reaction (a "PromiseReactionJob") — this is the exact moment your .then() callbacks actually get queued for execution, not before. This is also why calling .then() on an already-settled Promise still queues a fresh microtask immediately (rather than running synchronously) — the reaction-scheduling code path is identical either way, whether the reaction was already waiting or just got added after the fact.
Why a Promise can only ever settle once
resolve/reject themselves check [[PromiseState]] before doing anything — if it's no longer "pending", the call is a silent no-op. This single guard is the entire mechanism behind "once settled, a Promise never changes state again" — it isn't a convention, it's enforced by a state check on every single resolve/reject invocation, including ones that "lose the race" (see the Promises Part 2 combinator behavior, and Promise.all's internal counter in the Parallel vs Sequential topic).
1Promise.all — Wait for Everything, Fail Fast
const results = await Promise.all([
fetch("/api/users"),
fetch("/api/orders"),
fetch("/api/products"),
]);- Resolves with an array of all results, in the same order as the input, once every Promise fulfills.
- Rejects immediately with the first rejection reason if ANY Promise rejects — the other Promises keep running in the background, but their results are discarded by
Promise.allitself. - Use when you need all results and can't proceed with partial data.
2Promise.allSettled — Wait for Everything, Never Fails
const results = await Promise.allSettled([
fetch("/api/users"),
fetch("/api/might-fail"),
]);
// [{ status: "fulfilled", value: ... }, { status: "rejected", reason: ... }]Always resolves (never rejects) once every input Promise has settled, reporting each outcome individually. Use when partial failures are acceptable and you want to know about all of them.
3Promise.race — First to Settle Wins
const result = await Promise.race([
fetch("/api/data"),
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000)),
]);Settles (fulfilled OR rejected) as soon as the first input Promise settles, in either direction. The classic use case: implementing a timeout by racing a real operation against a timer that rejects.
4Promise.any — First to Fulfill Wins
const result = await Promise.any([
fetch("https://mirror1.example.com/data"),
fetch("https://mirror2.example.com/data"),
]);Resolves with the first fulfillment, ignoring rejections — only rejects if all input Promises reject (with an AggregateError containing all reasons). Use for "try several sources, take whichever succeeds first."
5Comparison Table
| Combinator | Resolves when | Rejects when |
|---|---|---|
Promise.all | all fulfill | any one rejects (immediately) |
Promise.allSettled | all settle (never rejects) | never |
Promise.race | first settles (fulfilled OR rejected) | first settles as a rejection |
Promise.any | first fulfills | all reject |
6Turning a Callback API into a Promise (Promisifying)
function readFilePromise(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, "utf8", (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}This exact wrapping pattern — resolve on success, reject on the error-first callback's error — is how legacy callback-based APIs are adapted to work with async/await (Node also provides util.promisify to automate it for standard error-first functions).
⚠️ Common Pitfalls
- Using
Promise.allwhen partial failure is acceptable — one rejection loses all other results; useallSettledinstead if you need to know about every outcome. - Forgetting
Promise.allruns everything CONCURRENTLY, not sequentially — all input Promises start immediately when the array is created, regardless ofawait; sequencing must be done explicitly if needed (see the Parallel vs Sequential topic). - Not handling rejection in a "fire and forget" Promise — an unhandled rejected Promise triggers an
unhandledRejectionwarning/crash in Node and browsers; always attach a.catch()or wrap intry/catchwithawait. - Wrapping an already-async function in
new Promise()unnecessarily — if a function already returns a Promise, just return it directly; wrapping it again ("Promise constructor antipattern") adds needless indirection and can swallow errors if done incorrectly.
📋 Quick Reference
Promise.all([...]) → all-or-nothing, fails fast
Promise.allSettled([...]) → always resolves, per-item status
Promise.race([...]) → first to settle, either way
Promise.any([...]) → first to succeed, ignores failures until all fail🎤 Interview Q&A
Q1What is a promise and its states?core▾
An object representing the eventual result of an async operation: pending, then settled as fulfilled (value) or rejected (reason). It settles once and stays settled; callbacks via then/catch/finally run asynchronously.
- Is the executor sync? Yes, the function passed to new Promise runs immediately.
Q2Compare Promise.all, allSettled, race and any.core▾
all: resolves with all values, rejects on the first rejection. allSettled: waits for all, returns statuses. race: settles with the first settled promise. any: resolves with the first fulfillment, rejects with AggregateError if all reject.
- Timeout pattern?
Promise.race([work, timeout(5000)])(with AbortController to cancel work).
Q3How do you promisify a callback API?core▾
new Promise((resolve, reject) => fs.readFile(p, (err, data) => err ? reject(err) : resolve(data))) or util.promisify.
- What's the explicit-construction anti-pattern? Wrapping existing promises in new Promise unnecessarily.
Q4What happens with unhandled rejections?senior▾
Browsers fire unhandledrejection and log errors; Node (v15+) crashes the process by default. Always attach handlers or await inside try/catch.
- Common source? Fire-and-forget async calls without catch.
Q5How do you implement Promise.all yourself?core▾
Return a new promise; track results by index and a counter; resolve when the counter equals the input length; reject on the first error; resolve immediately for an empty array.
- Why track by index? Results must keep input order regardless of completion order.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.