Promises
The Receipt That Runs Your Future Code
LinkedIn Hook
A Promise is the single most important data structure in modern JavaScript — and most developers who use
async/awaitevery day can't build one from scratch.That matters because
async/awaitis just syntactic sugar over Promises. The Promise is the actual object doing the work. If you don't understand the three states (pending, fulfilled, rejected), the fact that a Promise can only settle once, and how.then()returns a brand-new Promise that lets you chain, you can't reason about any async bug that gets interesting.In this lesson you'll learn what a Promise is, the lifecycle it goes through, how to convert nested callback hell into a flat Promise chain, the difference between
.then(),.catch(), and.finally(), and — for the truly curious — how to build a minimal Promise implementation from scratch.If you've ever been asked "can you implement Promise?" in an interview and frozen up — this lesson is the fix.
Read the full lesson -> [link]
#JavaScript #InterviewPrep #Promises #AsyncJS #Frontend #CodingInterview #WebDevelopment
What You'll Learn
- What a Promise is, the three states it can be in, and the one-way settlement rule
- How to chain
.then(),.catch(), and.finally()to replace nested callbacks with flat code - How to build a minimal Promise class from scratch to understand it from the inside out
What Is a Promise?
Think of a Promise like ordering food at a restaurant. When you order, the waiter gives you a receipt (the Promise). The food isn't ready yet — your receipt is in a pending state. Eventually one of two things happens: the kitchen successfully prepares your meal (fulfilled), or they're out of ingredients and can't make it (rejected). Either way, the receipt lets you plan what to do next.
Promise States
| State | Description | Settled? |
|---|---|---|
| pending | Initial state, operation in progress | No |
| fulfilled | Operation completed successfully | Yes |
| rejected | Operation failed | Yes |
A Promise can only transition once: pending -> fulfilled, or pending -> rejected. Once settled, it's immutable. Additional calls to resolve() or reject() are silently ignored.
Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("Operation completed!"); // -> fulfilled
} else {
reject(new Error("Something went wrong")); // -> rejected
}
});
myPromise
.then((result) => console.log(result)) // "Operation completed!"
.catch((error) => console.error(error));
Callback Hell -> Promise Chain (The Evolution)
Here's the same nested callback hell from the previous lesson rewritten with Promises:
// Step 1: Promisify the functions
function getUser(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: userId, name: "Rakibul" });
}, 500);
});
}
function getOrders(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([{ id: 101, item: "Laptop" }, { id: 102, item: "Phone" }]);
}, 500);
});
}
function getOrderDetails(orderId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: orderId, trackingId: "TRK-789", status: "shipped" });
}, 500);
});
}
// Step 2: Flat Promise chain — no nesting!
getUser(1)
.then((user) => {
console.log("User:", user.name);
return getOrders(user.id);
})
.then((orders) => {
console.log("First order:", orders[0].item);
return getOrderDetails(orders[0].id);
})
.then((details) => {
console.log("Tracking:", details.trackingId);
console.log("Status:", details.status);
})
.catch((error) => {
// ONE catch handles errors from ANY step above
console.error("Something failed:", error.message);
})
.finally(() => {
console.log("Done — cleanup here (loading spinner off, etc.)");
});
Key improvements over callbacks:
- Flat structure — no pyramid, reads top to bottom.
- Single error handler —
.catch()at the end catches any failure in the chain. - Composable — each
.then()returns a new Promise, so you can keep chaining. .finally()— runs regardless of success or failure (great for cleanup).
Building a Custom Promise from Scratch (Simplified)
Understanding how Promise works internally is a powerful interview signal:
class SimplePromise {
constructor(executor) {
this.state = "pending";
this.value = undefined;
this.callbacks = [];
const resolve = (value) => {
if (this.state !== "pending") return; // can only settle once
this.state = "fulfilled";
this.value = value;
this.callbacks.forEach((cb) => cb.onFulfilled(value));
};
const reject = (reason) => {
if (this.state !== "pending") return;
this.state = "rejected";
this.value = reason;
this.callbacks.forEach((cb) => cb.onRejected(reason));
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
return new SimplePromise((resolve, reject) => {
const handle = () => {
try {
if (this.state === "fulfilled") {
const result = onFulfilled
? onFulfilled(this.value)
: this.value;
resolve(result);
}
if (this.state === "rejected") {
if (onRejected) {
const result = onRejected(this.value);
resolve(result);
} else {
reject(this.value);
}
}
} catch (err) {
reject(err);
}
};
if (this.state === "pending") {
this.callbacks.push({
onFulfilled: () => handle(),
onRejected: () => handle(),
});
} else {
// already settled — schedule microtask
queueMicrotask(handle);
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
}
// Test it
const p = new SimplePromise((resolve) => {
setTimeout(() => resolve("It works!"), 1000);
});
p.then((val) => console.log(val)); // "It works!" after 1 second
Common Mistakes
- Calling
resolve()orreject()multiple times and expecting both to take effect — the second call is silently ignored because a Promise can only settle once. - Forgetting that a
.then()callback that returns a value produces a brand-new Promise — mutating shared state inside.then()instead of returning a chained value breaks composition. - Treating
.catch()as a try/catch —.catch()only catches rejections from preceding steps in the chain, so a.catch()placed before a later.then()won't see failures in that later step.
Interview Questions
Q: What are the three states of a Promise?
Pending (initial), fulfilled (resolved successfully), rejected (failed). Once a Promise settles (fulfilled or rejected), its state and value are immutable.
Q: What is the difference between .then(), .catch(), and .finally()?
.then(onFulfilled, onRejected)handles fulfillment (and optionally rejection)..catch(onRejected)is sugar for.then(null, onRejected)— handles only rejections..finally(callback)runs after settlement regardless of outcome and receives no arguments — ideal for cleanup like hiding loading spinners.
Q: What happens if you return a value inside .then()?
It gets automatically wrapped in a resolved Promise, so the next
.then()in the chain receives it. If you return a Promise, the chain waits for it to settle.
Q: Can a Promise be resolved more than once?
No. Once a Promise transitions from pending to fulfilled or rejected, it's settled permanently. Additional calls to
resolve()orreject()are silently ignored.
Q: Can a Promise change from fulfilled to rejected?
No. Settlement is a one-way, one-time transition. Once fulfilled, a Promise stays fulfilled; once rejected, it stays rejected.
Q: What does .finally() do?
Runs after the Promise settles — fulfilled or rejected — without receiving the value or reason. It's the cleanup hook: close modals, stop spinners, release resources, regardless of outcome.
Quick Reference — Cheat Sheet
PROMISES — QUICK MAP
States:
pending -> fulfilled (resolve)
-> rejected (reject)
Once settled, IMMUTABLE.
Chain shape:
doSomething()
.then(result => ...) // on success
.catch(error => ...) // on failure (any step)
.finally(() => ...) // always runs
Key rules:
- Each .then() returns a NEW Promise (chainable)
- Returning a value inside .then() wraps it in Promise.resolve()
- Returning a Promise inside .then() waits for it
- .catch(h) == .then(null, h)
- .finally() receives no arguments
Previous: Callbacks & Callback Hell Next: async/await — Syntactic Sugar Over Promises
This is Lesson 3.7 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.