A Promise is an object that represents the eventual result of an asynchronous operation. Think of it like ordering food at a restaurant — you get a receipt (the promise) immediately, and the food (the result) comes later. The receipt can either be fulfilled (food arrives) or rejected (kitchen is closed).
Promise States
A Promise is always in one of three states:
Creating a Promise
We create a Promise using the new Promise() constructor. It takes a function with two parameters: resolve (for success) and reject (for failure).
const myPromise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("It worked!"); // fulfilled
} else {
reject("Something went wrong"); // rejected
}
});
Consuming a Promise: .then(), .catch(), .finally()
.then(callback)— runs when the Promise is fulfilled.catch(callback)— runs when the Promise is rejected.finally(callback)— runs no matter what (fulfilled or rejected)
myPromise
.then(result => console.log(result)) // "It worked!"
.catch(error => console.log(error)) // runs if rejected
.finally(() => console.log("Done!")); // always runs
Promise Chaining
The real magic of Promises is chaining. Whatever we return from a .then() gets passed as the input to the next .then(). This flattens the callback hell into a clean chain.
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => getShippingInfo(details.trackingId))
.then(shipping => console.log(shipping.status))
.catch(error => console.log("Something failed:", error));
Compare this with the callback hell version — night and day difference. Each .then() returns a new Promise, so we can keep chaining.
Error Propagation
One of the best things about Promises is that a single .catch() at the end catches errors from any step in the chain. If step 2 fails, it skips all remaining .then() calls and jumps straight to .catch().
fetchData()
.then(data => processData(data)) // if this throws...
.then(result => saveResult(result)) // ...this is skipped
.then(() => console.log("Saved!")) // ...this is skipped too
.catch(error => {
console.log("Caught:", error); // ...and we land here
});
Converting Callbacks to Promises
We can wrap any callback-based function in a Promise. This is a very useful pattern:
// Callback-based
function loadData(url, callback) {
fetch(url, (err, data) => callback(err, data));
}
// Promise-based wrapper
function loadData(url) {
return new Promise((resolve, reject) => {
fetch(url, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Now we can use it with .then()
loadData("/api/users")
.then(data => console.log(data))
.catch(err => console.log(err));
Quick tips
- A
.then()can take two arguments:then(onFulfilled, onRejected)— but using.catch()is cleaner and more readable. - If we return a plain value from
.then(), the next.then()gets it wrapped in a resolved Promise automatically. - If we return a Promise from
.then(), the next.then()waits for it to settle.
In simple language, Promises give us a way to handle async operations without the nesting nightmare. We chain .then() calls for sequential steps and use a single .catch() at the end for errors. They were such a big improvement over callbacks that they became the foundation for async/await.