Promises
In the last lesson you saw how nested callbacks turn into "callback hell". A promise is the modern answer. It is an object that stands for a result that is not ready yet, like the token you get at a food court counter. You can hold on to it, pass it around, and say what should happen when the result arrives or when something goes wrong.
In this lesson you will use promises with then, catch and finally, create your own, chain several steps without nesting, and run tasks in parallel with Promise.all and friends. You will also meet Promise.withResolvers and the new Promise.try. Read How Asynchronous Code Works first.
The three states of a promise
A promise is always in one of three states:
- Pending: the work is still going on.
- Fulfilled: the work succeeded, and the promise holds a value.
- Rejected: the work failed, and the promise holds a reason, usually an
Error.
Once a promise is fulfilled or rejected, it is settled and can never change again. You react to it with three methods:
.then(callback)runs when the promise is fulfilled and receives the value..catch(callback)runs when it is rejected and receives the error..finally(callback)runs either way, which is useful for clean-up such as hiding a loading spinner.
Creating a promise
Most promises you use come from built-in functions such as fetch(). Sometimes you need to wrap older, callback-based code yourself. new Promise() takes a function, called the executor, which receives two functions: call resolve(value) on success and reject(error) on failure.
function cookOrder(dish) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (dish === "Pizza") {
reject(new Error("Sorry, pizza is not on the menu"));
} else {
resolve(`${dish} is ready`);
}
}, 500);
});
}
const order = cookOrder("Masala dosa");
console.log(order instanceof Promise);
order
.then((message) => console.log(message))
.catch((error) => console.log(error.message))
.finally(() => console.log("Token 1 closed"));
cookOrder("Pizza")
.then((message) => console.log(message))
.catch((error) => console.log(error.message))
.finally(() => console.log("Token 2 closed"));true Masala dosa is ready Token 1 closed Sorry, pizza is not on the menu Token 2 closed
cookOrder returns the promise straight away, while the dosa is still "cooking". The callbacks run half a second later. Always reject with an Error object rather than a plain string, so the error has a message and a stack trace.
A very handy small helper you will see often is a promise-based pause: const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));.
Chaining instead of nesting
Here is the important part: .then() always returns a new promise. Whatever your callback returns becomes the value for the next .then(). If your callback returns another promise, the chain waits for it. That lets you write dependent steps as a flat list instead of a pyramid.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function login(name) {
return wait(300).then(() => ({ id: 7, name }));
}
function getOrders(userId) {
return wait(300).then(() => [
{ id: 501, item: "Cricket bat" },
{ id: 502, item: "Kit bag" },
]);
}
function getDelivery(order) {
return wait(300).then(() => {
if (order.id === 502) {
throw new Error(`No courier found for order ${order.id}`);
}
return `${order.item} arrives on Friday`;
});
}
login("priya")
.then((user) => getOrders(user.id))
.then((orders) => getDelivery(orders[0]))
.then((status) => console.log(status))
.catch((error) => console.log(`Problem: ${error.message}`))
.finally(() => console.log("Done"));Cricket bat arrives on Friday Done
Compare this with the nested callback version from the previous lesson. Each step sits on its own line, and one .catch() at the end handles an error from any step. Change orders[0] to orders[1] and run it again: getDelivery throws, the remaining .then() is skipped, and the chain jumps straight to .catch(), printing "Problem: No courier found for order 502".
Running promises in parallel
Steps that do not depend on each other should run at the same time. Four static methods take an array of promises and return one combined promise:
| Method | Fulfils when | Rejects when |
|---|---|---|
Promise.all() | All fulfil; gives an array of values in the original order | Any one rejects (it fails fast) |
Promise.allSettled() | All have settled; gives an array of { status, value } or { status, reason } | Never |
Promise.any() | The first one fulfils | All reject; gives an AggregateError |
Promise.race() | The first one to settle fulfils | The first one to settle rejects |
This example asks three shops for a price. One shop is closed:
function checkShop(name, ms, price) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (price === null) {
reject(new Error(`${name} is closed`));
} else {
resolve({ name, price });
}
}, ms);
});
}
// A fresh set of three requests each time it is called
const askShops = () => [
checkShop("Gupta Sports", 300, 1299),
checkShop("Kochi Kits", 100, 1249),
checkShop("Rohtak Traders", 200, null),
];
Promise.all(askShops())
.then((results) => console.log(`all: ${results.length} prices`))
.catch((error) => console.log(`all failed: ${error.message}`));
Promise.allSettled(askShops()).then((results) => {
console.log(`allSettled: ${results.map((r) => r.status).join(", ")}`);
});
Promise.any(askShops()).then((first) => {
console.log(`any: first price from ${first.name}`);
});
Promise.race(askShops()).then((first) => {
console.log(`race: first answer from ${first.name}`);
});any: first price from Kochi Kits race: first answer from Kochi Kits all failed: Rohtak Traders is closed allSettled: fulfilled, fulfilled, rejected
How to choose:
- Use
Promise.all()when you need every result and one failure makes the whole job useless. - Use
Promise.allSettled()when you want every result you can get, such as a dashboard where one broken widget should not hide the others. - Use
Promise.any()when any one success is enough, such as trying two mirror servers. - Use
Promise.race()mainly to compete a task against a timer.
Promise.withResolvers
Sometimes the code that should resolve a promise lives outside the executor function, for example in an event listener or another object. Promise.withResolvers() (added in ES2024) returns the promise and its resolve and reject functions together, so you can hand them to whoever needs them.
function createGate() {
const { promise, resolve } = Promise.withResolvers();
return { opened: promise, open: resolve };
}
const gate = createGate();
gate.opened.then((who) => console.log(`Gate opened by ${who}`));
console.log("Waiting at the gate...");
setTimeout(() => gate.open("the guard"), 500);Waiting at the gate... Gate opened by the guard
Common mistakes
- Forgetting to
returninside.then(). Withoutreturn getOrders(id), the next step getsundefinedand does not wait. - Nesting
.then()inside.then(). Return the promise and chain instead. - No
.catch()at the end. A rejection with no handler becomes an "unhandled rejection" error in the console. - Wrapping a promise in
new Promise()for no reason. If a function already returns a promise, just return it or chain on it. - Running independent steps one after another. Start them together and use
Promise.all().
Key points
- A promise is pending, then settles once as fulfilled (with a value) or rejected (with an error).
thenhandles success,catchhandles failure andfinallyruns either way..then()returns a new promise, so steps can be chained, and one.catch()covers the whole chain.Promise.all,allSettled,anyandracecombine several promises running in parallel.Promise.withResolvers()hands youresolveandrejectoutside the executor;Promise.try()turns any function into a promise.
Exercise
Write a function bookSeat(train) that returns a promise. After 400 ms it should resolve with "Seat booked on Rajdhani" when the train name is "Rajdhani" and reject with an Error for any other name. Then use Promise.allSettled() to book "Rajdhani" and "Ghost Express" at the same time, and log one line per result.
Show solution
function bookSeat(train) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (train === "Rajdhani") {
resolve(`Seat booked on ${train}`);
} else {
reject(new Error(`${train} is not running today`));
}
}, 400);
});
}
Promise.allSettled([bookSeat("Rajdhani"), bookSeat("Ghost Express")]).then((results) => {
for (const result of results) {
console.log(result.status === "fulfilled" ? result.value : result.reason.message);
}
});This prints "Seat booked on Rajdhani" and then "Ghost Express is not running today". allSettled never rejects, so you can inspect every result, successful or not.
Quick Quiz
1. Which state can a settled promise move to next?
Once a promise is fulfilled or rejected, its state and value are fixed.
2. What does .then() return?
Every .then() returns a new promise, which is what makes chaining possible.
3. Three requests run with Promise.all() and the second one rejects. What happens?
Promise.all() fails fast as soon as any input promise rejects.
4. You want results from five widgets even if some fail. Which method fits?
allSettled waits for every promise and reports each one as fulfilled or rejected.
Learn JavaScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
