Promises
Learn how Promises work in JavaScript and how they solve the problems of callback-based async code.
Promises
In the previous topic we saw that callbacks get messy fast when you chain multiple async operations. Promises were introduced in ES6 specifically to fix this.
A Promise is an object that represents the eventual result of an async operation. It is a placeholder for a value that does not exist yet — but will at some point in the future.
Think of it like ordering food at a restaurant. The waiter gives you a receipt — that is the Promise. You do not have the food yet, but you have a guarantee that it is coming. When it is ready — you get it. If the kitchen runs out — you get an apology.
const promise = fetch("https://api.example.com/user");
// promise is not the data yet — it is a guarantee that data is comingThree States of a Promise
Every Promise is always in one of three states:
- Pending — the operation has started but not finished yet
- Fulfilled — the operation completed successfully — a value is available
- Rejected — the operation failed — an error is available
A Promise starts as pending and settles into either fulfilled or rejected. Once settled it never changes state again.
Creating a Promise
You create a Promise with new Promise(). It takes a function called the executor — which receives two arguments: resolve and reject.
- Call
resolve(value)when the operation succeeds - Call
reject(error)when it fails
const promise = new Promise((resolve, reject) => {
// do some async work...
const success = true;
if (success) {
resolve("Operation succeeded!"); // fulfilled
} else {
reject(new Error("Something went wrong.")); // rejected
}
});A real async example — simulating a server request
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id <= 0) {
reject(new Error("Invalid user ID."));
return;
}
const user = { id, name: "Ali", email: "ali@example.com" };
resolve(user);
}, 1000);
});
}fetchUser returns a Promise. After 1 second it either resolves with a user object or rejects with an error.
Consuming a Promise — .then() and .catch()
Once you have a Promise you use .then() to handle the fulfilled value and .catch() to handle errors.
fetchUser(1)
.then(user => {
console.log("Got user:", user);
})
.catch(error => {
console.log("Error:", error.message);
});
// (after 1 second)
// Got user: { id: 1, name: "Ali", email: "ali@example.com" }fetchUser(-1)
.then(user => {
console.log("Got user:", user);
})
.catch(error => {
console.log("Error:", error.message);
});
// (after 1 second)
// Error: Invalid user ID..then() only runs when the Promise is fulfilled. .catch() only runs when it is rejected.
.finally() — Always Runs
.finally() runs regardless of whether the Promise fulfilled or rejected. Perfect for cleanup code — hiding a loading spinner, closing a connection.
fetchUser(1)
.then(user => {
console.log("User:", user.name);
})
.catch(error => {
console.log("Error:", error.message);
})
.finally(() => {
console.log("Done — hide loading spinner");
loadingSpinner.style.display = "none";
});.finally() does not receive any value — it just runs when the Promise settles.
Promise Chaining
This is where Promises shine over callbacks. When .then() returns a value — that value is automatically wrapped in a new Promise. This lets you chain multiple async operations cleanly and flatly.
fetchUser(1)
.then(user => {
console.log("Got user:", user.name);
return fetchPosts(user.id); // return a new Promise
})
.then(posts => {
console.log("Got posts:", posts.length);
return fetchComments(posts[0].id); // return another Promise
})
.then(comments => {
console.log("Got comments:", comments.length);
})
.catch(error => {
console.log("Something failed:", error.message);
});Compare this to the callback version from the previous topic — same operations, but flat instead of nested. One .catch() at the end handles errors from any step in the chain.
What you can return from .then()
fetchUser(1)
.then(user => user.name) // return a plain value
.then(name => name.toUpperCase()) // gets the plain value
.then(name => {
console.log(name); // ALI
return fetchPosts(1); // return a Promise
})
.then(posts => {
console.log(posts); // gets the resolved value of fetchPosts
});Whatever you return from .then() becomes the value the next .then() receives — whether it is a plain value or another Promise.
Error Propagation in Chains
If any step in a chain throws an error or returns a rejected Promise, the chain skips all remaining .then() calls and jumps straight to .catch().
fetchUser(1)
.then(user => {
throw new Error("Something broke here"); // manually throw
})
.then(data => {
console.log("This never runs");
})
.then(data => {
console.log("This never runs either");
})
.catch(error => {
console.log("Caught:", error.message); // Caught: Something broke here
});This is a massive improvement over callbacks — one error handler covers the entire chain.
Promise.resolve() and Promise.reject()
Create an already-settled Promise instantly — useful for testing and wrapping non-Promise values.
// Already fulfilled
Promise.resolve("instant value")
.then(value => console.log(value)); // instant value
// Already rejected
Promise.reject(new Error("instant error"))
.catch(error => console.log(error.message)); // instant errorRunning Multiple Promises
Promise.all() — run in parallel, wait for all
Takes an array of Promises and returns a new Promise that fulfills when all of them fulfill. If any one rejects — the whole thing rejects.
const userPromise = fetchUser(1);
const postsPromise = fetchPosts(1);
const settingsPromise = fetchSettings(1);
Promise.all([userPromise, postsPromise, settingsPromise])
.then(([user, posts, settings]) => {
// all three resolved — destructure results
console.log(user.name);
console.log(posts.length);
console.log(settings.theme);
})
.catch(error => {
console.log("One of them failed:", error.message);
});All three requests run at the same time — not one after another. This is much faster than chaining them sequentially.
Promise.allSettled() — wait for all, regardless of outcome
Like Promise.all() but never rejects. Waits for every Promise to settle and gives you the result of each one — whether fulfilled or rejected.
Promise.allSettled([fetchUser(1), fetchUser(-1), fetchPosts(1)])
.then(results => {
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.log("Failed:", result.reason.message);
}
});
});Use allSettled when you want all results even if some fail — like loading multiple independent parts of a dashboard.
Promise.race() — first one wins
Returns the result of whichever Promise settles first — fulfilled or rejected.
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Request timed out")), 5000)
);
Promise.race([fetchUser(1), timeout])
.then(user => console.log("Got user:", user.name))
.catch(error => console.log("Error:", error.message));If fetchUser takes more than 5 seconds, timeout rejects first and .catch() handles it. This is a common pattern for adding timeouts to requests.
Promise.any() — first fulfilled wins
Returns the first Promise that fulfills. Only rejects if all of them reject.
Promise.any([
fetchFromServer1(),
fetchFromServer2(),
fetchFromServer3()
])
.then(data => console.log("Got data from the fastest server:", data))
.catch(error => console.log("All servers failed"));Useful when you have multiple sources and just need the first successful one.
Promise Methods Summary
| Method | Behavior |
|---|---|
Promise.all([...]) | Waits for all — rejects if any fails |
Promise.allSettled([...]) | Waits for all — never rejects, gives all results |
Promise.race([...]) | Returns first to settle — fulfilled or rejected |
Promise.any([...]) | Returns first to fulfill — rejects only if all fail |
A Real Example — Loading a Dashboard
function showDashboard(userId) {
const loadingEl = document.getElementById("loading");
const dashboardEl = document.getElementById("dashboard");
loadingEl.style.display = "block";
Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId)
])
.then(([user, posts, notifications]) => {
dashboardEl.innerHTML = `
<h1>Welcome, ${user.name}!</h1>
<p>You have ${posts.length} posts.</p>
<p>You have ${notifications.length} notifications.</p>
`;
})
.catch(error => {
dashboardEl.innerHTML = `
<p class="error">Failed to load dashboard: ${error.message}</p>
`;
})
.finally(() => {
loadingEl.style.display = "none";
});
}
showDashboard(1);All three requests fire simultaneously with Promise.all. The dashboard renders when all data is ready. Errors are handled in one place. The loading spinner disappears no matter what happens. Clean, readable, and real.
In modern JavaScript, async/await is the preferred way to work with Promises. But Promises are the foundation — async/await is just cleaner syntax built on top of them. Understanding Promises deeply makes async/await much easier to understand.
Summary
- A Promise is an object representing the eventual result of an async operation
- Three states — pending, fulfilled, rejected — settles once and never changes
- Create with
new Promise((resolve, reject) => {})— callresolveon success,rejecton failure .then()handles the fulfilled value —.catch()handles errors —.finally()always runs- Chain
.then()calls to run async operations in sequence — flat and readable - One
.catch()at the end of a chain handles errors from any step Promise.all()— run in parallel, wait for allPromise.allSettled()— wait for all, get every result including failuresPromise.race()— first to settle winsPromise.any()— first to fulfill wins