What Is a Promise
A Promise is a special object in JavaScript that represents the result of an async task that will finish later. That task can be an API call, a file read, a database query, or anything that takes time.
Think of the word promise in real life. If I promise to send you a book tomorrow, you don't have the book right now — but you trust that at some point you will either get the book or hear that I failed. A JavaScript promise works the same way.
When you call an async function that returns a promise, you get back an empty box right away. Later, when the task finishes, JavaScript fills that box with either the result (success) or an error (failure). Your code can attach handlers to this box to run automatically when the data arrives.
Why Were Promises Created
Before promises, developers handled async tasks using callbacks. A callback is just a function you pass to another function to be called later. Callbacks work, but they have two big problems.
Problem 1: Inversion of Control
When you pass a callback to some other API, you give that API full control of your function. You trust that it will call your function exactly once, at the right time. But what if it calls your function twice? What if it never calls it? What if it calls it with wrong data? You have no guarantee.
Problem 2: Callback Hell
When async tasks depend on each other, callbacks nest inside callbacks inside more callbacks. The code starts moving to the right in a pyramid shape — hard to read, hard to debug, hard to change.
Promises solve both problems. They give you back control, they guarantee the callback runs only once, and they let you chain async steps in a clean vertical flow instead of a nested mess.
The Three States of a Promise
A promise is always in one of three states. It starts in one state and changes only once — after that, the state is locked forever.
1. pending
2. fulfilled
3. rejected
Every promise starts as pending. Then it moves to fulfilled or rejected — and that's final. It can never go back. This rule is what makes promises so trustworthy.
Your First Promise: The fetch Function
The simplest way to see a promise in action is the browser's fetch function. It makes an HTTP request and returns a promise.
const userPromise = fetch("https://api.github.com/users/octocat");
console.log(userPromise);
// Promise { state: "pending", result: undefined }
userPromise.then(response => response.json())
.then(data => console.log(data.name));
// Logs: "The Octocat" (after a few hundred ms)
On the very next line, the promise is still pending. But within a few milliseconds the request finishes and the promise becomes fulfilled. Your .then handler runs automatically.
How Promises Work: Step by Step
Let's walk through what happens when a promise is used.
Callbacks vs Promises: A Clear Example
Let's say we have an e-shop with two steps: create an order and start payment. The second step needs the order ID from the first.
The Old Callback Way
const cart = ["shoes", "watch", "bag"];
createOrder(cart, function (orderId) {
startPayment(orderId, function (paymentId) {
console.log("Payment done:", paymentId);
});
});
You hand over your function to createOrder and hope it behaves. You have no guarantee.
The Promise Way
const cart = ["shoes", "watch", "bag"];
createOrder(cart)
.then(orderId => startPayment(orderId))
.then(paymentId => console.log("Payment done:", paymentId))
.catch(err => console.log("Something failed:", err));
Now the control stays with you. You attach your handler to the promise. JavaScript itself guarantees it will run only once, and only at the right time.
Creating Your Own Promise
You can also build a promise yourself using the new Promise constructor. It takes a function with two arguments: resolve (call this on success) and reject (call this on failure).
function wait(ms) {
return new Promise((resolve, reject) => {
if (ms < 0) {
reject("Time cannot be negative");
} else {
setTimeout(() => resolve("Done after " + ms + " ms"), ms);
}
});
}
wait(1000)
.then(msg => console.log(msg))
.catch(err => console.log("Error:", err));
// After 1 second: "Done after 1000 ms"
This small wait function wraps setTimeout in a promise. You can now use .then and .catch on it just like any built-in promise.
Promise Chaining: Escaping Callback Hell
Every .then itself returns a new promise. That means you can line them up one after another in a vertical chain. Each step passes its result to the next.
Real Example: Login → Profile → Posts
loginUser("anita", "pass123")
.then(user => getProfile(user.id))
.then(profile => getPosts(profile.username))
.then(posts => console.log("Latest posts:", posts))
.catch(err => console.log("Failed at some step:", err));
Read it top to bottom: "Login, then load the profile, then load the posts, then print them — and if anything fails, catch it." One single .catch at the bottom handles errors from any step in the chain.
Important: Always Return the Next Promise
If a step does more async work, you must return that new promise. Otherwise the chain loses the data.
// Wrong: forgot the return
fetch("/user").then(res => {
res.json().then(data => console.log(data));
});
// Right: return the inner promise
fetch("/user")
.then(res => res.json())
.then(data => console.log(data));
Handling Errors with .catch
When a promise is rejected, the closest .catch further down the chain handles it. Any .then in between is skipped.
fetch("https://bad-url-that-does-not-exist.test")
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.log("Network failed:", err.message));
You can also add a .finally at the end to run code no matter what happens — great for hiding loading spinners.
showSpinner();
fetch("/api/data")
.then(res => res.json())
.then(data => render(data))
.catch(err => showError(err))
.finally(() => hideSpinner());
Useful Built-in Promise Helpers
| Helper | What It Does |
|---|---|
Promise.all([p1, p2]) |
Wait for all to succeed; fail fast if any one rejects |
Promise.allSettled([p1, p2]) |
Wait for all to finish — success or failure — and give you the full list |
Promise.race([p1, p2]) |
Finish as soon as the first one finishes (success or failure) |
Promise.any([p1, p2]) |
Finish as soon as the first one succeeds |
Promise.resolve(value) |
Quick promise already fulfilled with a value |
Promise.reject(err) |
Quick promise already rejected with an error |
Example: Running Many Requests at Once
const urls = [
"/api/user",
"/api/notifications",
"/api/cart"
];
Promise.all(urls.map(u => fetch(u).then(r => r.json())))
.then(([user, notifs, cart]) => {
console.log(user, notifs, cart);
})
.catch(err => console.log("At least one failed:", err));
Common Mistakes
Mistake 1: Nesting .then Instead of Chaining
Nesting brings back the callback pyramid. Always flatten the chain.
// Wrong: nested .then
getUser().then(user => {
getOrders(user.id).then(orders => {
console.log(orders);
});
});
// Right: flat chain
getUser()
.then(user => getOrders(user.id))
.then(orders => console.log(orders));
Mistake 2: Forgetting .catch
Without a .catch, errors silently disappear and you waste hours debugging. Always end a chain with one.
Mistake 3: Not Returning the Inner Promise
.then, if you start another async task, you must return it. Otherwise the next .then runs too early and receives undefined.
Mistake 4: Trying to Change a Resolved Promise
Once a promise is fulfilled or rejected, it stays that way forever. You cannot "reset" it. Calling resolve or reject twice has no effect.
Mistake 5: Mixing Callbacks and Promises
If a function returns a promise, just use .then. Don't also pass it a callback — you will confuse yourself and double-run your code.
Comparison: Callbacks vs Promises vs async/await
| Feature | Callbacks | Promises | async/await |
|---|---|---|---|
| Readability | Pyramid of doom | Clean chain | Looks synchronous |
| Error handling | Manual in every callback | Single .catch |
Standard try / catch |
| Control | Given to other function | Stays with you | Stays with you |
| Runs callback only once? | No guarantee | Guaranteed | Guaranteed |
| Parallel work | Very hard | Promise.all |
Promise.all + await |
async/await is built on top of promises. Once you know promises, async/await is easy to pick up.
Real-World Usage
Loading User Data in a React App
useEffect(() => {
fetch("/api/profile")
.then(res => res.json())
.then(data => setProfile(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
Running Multiple Uploads in Parallel
function uploadFile(file) {
return fetch("/upload", { method: "POST", body: file });
}
Promise.all(files.map(uploadFile))
.then(() => console.log("All files uploaded"))
.catch(err => console.log("At least one upload failed:", err));
Timing Out a Slow Request
const timeout = new Promise((_, reject) =>
setTimeout(() => reject("Timed out!"), 5000)
);
Promise.race([fetch("/slow-api"), timeout])
.then(res => console.log("Got it in time"))
.catch(err => console.log(err));
The Interview Answer
Say this one line — slowly and confidently:
"A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value."
This is the official MDN definition. It sounds professional, covers both success and failure, and matches what interviewers want to hear.
Summary
A Promise is a JavaScript object that stands in for a value you don't have yet. It starts in the pending state and settles into exactly one of two final states: fulfilled (success) or rejected (failure). Once settled, it never changes again.
Promises were introduced to solve two painful problems with callbacks. They give you back control — you attach your handler instead of passing it away — and they let you chain async steps in a clean vertical flow instead of a nested pyramid.
The three main methods you will use almost every day are .then for success, .catch for errors, and .finally for cleanup. Helpers like Promise.all, Promise.race, Promise.allSettled, and Promise.any handle groups of promises.
Just remember the small rules: always return the inner promise inside a chain, always add a .catch, never try to change a settled promise, and never mix callbacks with promises. Once these habits click, you are ready to move on to async / await — which is just promises wearing a friendlier syntax.
| Concept | Key Takeaway |
|---|---|
| Promise | Object representing eventual completion of an async task |
| Three states | pending → fulfilled or rejected (locked forever) |
| .then() | Runs when promise is fulfilled |
| .catch() | Runs when promise is rejected |
| .finally() | Runs either way — useful for cleanup |
| Chaining | Each .then returns a new promise — no more nesting |
| Always return | Return the inner promise to keep the chain alive |
| Promise.all | Run many promises in parallel |
| Inversion of control | Callbacks have it, promises fix it |
| Interview line | "Eventual completion of an asynchronous operation" |