A promise represents a value that may be available now, later, or never. It is an object that acts as a placeholder for the result of an asynchronous operation.
Promises have three states:
- pending — the operation is still in progress
- fulfilled — the operation completed successfully
- rejected — the operation failed
Once a promise is fulfilled or rejected, it cannot change state.
Creating promises
You can create a promise with the Promise constructor:
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Ada" });
} else {
reject(new Error("Invalid user ID"));
}
}, 1000);
});
}
The executor function receives two arguments:
resolve(value)— call this when the operation succeedsreject(error)— call this when the operation fails
In practice, you rarely create promises manually. APIs like fetch() already return promises.
Consuming promises with .then() and .catch()
The .then() method attaches a callback that runs when the promise fulfills:
fetchUser(1).then((user) => {
console.log(user); // { id: 1, name: "Ada" }
});
The .catch() method attaches a callback that runs when the promise rejects:
fetchUser(-1).catch((error) => {
console.error(error.message); // "Invalid user ID"
});
Chaining promises
.then() returns a new promise. If the callback returns a value, the new promise fulfills with that value. If the callback returns a promise, the new promise adopts that promise’s state:
fetchUser(1)
.then((user) => {
console.log(user.name); // "Ada"
return fetchOrders(user.id); // returns a promise
})
.then((orders) => {
console.log(orders);
return orders[0];
})
.then((firstOrder) => {
console.log(firstOrder);
})
.catch((error) => {
console.error("Something went wrong:", error);
});
Each .then() receives the result of the previous one. Errors at any step skip to the nearest .catch().
.finally()
.finally() runs regardless of whether the promise fulfilled or rejected:
fetchUser(1)
.then((user) => console.log(user))
.catch((error) => console.error(error))
.finally(() => {
console.log("Request complete"); // always runs
});
Use .finally() for cleanup — hiding loading spinners, closing connections, or resetting state.
Parallel promises with Promise.all
When you have multiple independent async operations, Promise.all waits for all of them:
const userPromise = fetchUser(1);
const ordersPromise = fetchOrders(1);
Promise.all([userPromise, ordersPromise])
.then(([user, orders]) => {
console.log(user, orders);
})
.catch((error) => {
console.error("One of the promises failed:", error);
});
If any promise rejects, Promise.all immediately rejects with that error.
Promise.allSettled
Promise.allSettled waits for all promises to settle (fulfill or reject) and returns the result of each:
Promise.allSettled([
fetchUser(1),
fetchUser(-1), // will reject
])
.then((results) => {
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.log("Failed:", result.reason);
}
});
});
Use allSettled when you want results from all operations regardless of individual failures.
Promise.race
Promise.race settles as soon as the first promise settles:
Promise.race([
fetchUser(1),
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 5000)),
])
.then((user) => console.log(user))
.catch((error) => console.error(error.message));
This is commonly used to implement timeouts — if the fetch takes too long, the timeout promise rejects first.
Promise.any
Promise.any fulfills as soon as the first promise fulfills. It only rejects if all promises reject:
Promise.any([
fetchFromServer1(),
fetchFromServer2(),
fetchFromServer3(),
])
.then((result) => console.log("Fastest server responded:", result))
.catch((error) => console.error("All servers failed"));
Promise.withResolvers() (ES2024)
Sometimes you need to create a promise and control its resolution from outside the executor. Promise.withResolvers() gives you a promise along with its resolve and reject functions:
const { promise, resolve, reject } = Promise.withResolvers();
// Later, from anywhere:
resolve("done");
// or: reject(new Error("failed"));
This replaces the older pattern of wrapping new Promise in an object to expose resolve/reject:
// Old pattern — verbose
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
Promise.withResolvers() is useful for converting callback-based APIs to promises or building deferred patterns.
Common promise mistakes
Forgetting to return from .then()
fetchUser(1)
.then((user) => {
fetchOrders(user.id); // promise created but not returned
})
.then((orders) => {
console.log(orders); // undefined — previous .then() returned nothing
});
Fix:
fetchUser(1)
.then((user) => {
return fetchOrders(user.id); // return the promise
})
.then((orders) => {
console.log(orders);
});
Unhandled rejections
If a promise rejects and there is no .catch(), the error is swallowed in some environments and crashes in others:
fetchUser(-1).then((user) => console.log(user));
// UnhandledPromiseRejectionWarning
Always attach a .catch() or use async/await with try/catch.
What to carry forward
- a promise represents a future value — pending, fulfilled, or rejected
.then()attaches a success callback;.catch()attaches an error callback.then()returns a new promise — chain them for sequential async operations.finally()runs regardless of outcome — use it for cleanupPromise.allwaits for all promises — fails fast if any rejectsPromise.allSettledwaits for all and returns every resultPromise.racesettles with the first settled promisePromise.anyfulfills with the first fulfilled promisePromise.withResolvers()creates a promise with externally accessibleresolveandreject(ES2024)- always return promises from
.then()callbacks to chain them - always handle rejections with
.catch()
Promises replace nested callbacks with flat chains. The next lesson covers async/await, which makes promises look like synchronous code.