Promises in JavaScript
Every time you fetch data from a server, load an image, or wait for a user action, you're dealing with Promises. They're the backbone of modern JavaScript - you literally cannot build a web app without understanding them.
Start independent asynchronous operations concurrently instead of awaiting each one sequentially. Then choose the combinator based on what counts as success.
| Need | Use | Settles when |
|---|---|---|
| Every operation must succeed | Promise.all | All fulfill; rejects as soon as one rejects. |
| Keep every success and failure | Promise.allSettled | Every input settles; returns an outcome for each. |
| First result, success or failure | Promise.race | The first input settles. |
| First successful result | Promise.any | The first input fulfills; rejects with AggregateError if all reject. |
const primary = fetch("https://api.example.com/data");
const backup = fetch("https://backup.example.com/data");
Promise.any([primary, backup])
.then(response => response.json())
.then(data => console.log("First available response:", data))
.catch(error => {
console.error("Every source failed:", error.errors);
});
Concurrency example: if a dashboard needs users, settings, and notifications but none depends on another, start all three before awaiting them. If the settings request needs the user ID, await the user first, then start the dependent request.
async function loadDashboard() {
const [users, settings, notifications] = await Promise.all([
getUsers(),
getSettings(),
getNotifications()
]);
return { users, settings, notifications };
}
AbortController when unfinished work is no longer needed.
A Promise in JavaScript represents the eventual result of an asynchronous operation. It can eventually be fulfilled with a value or rejected with a reason. Your code can attach handlers for that eventual result without treating the Promise itself as the operation that performs the work.
Before Promises, developers relied heavily on callbacks. When many asynchronous steps were nested, the control flow could become harder to read and maintain, often called callback hell.
getUser(1, function(user) {
getOrders(user.id, function(orders) {
getOrderDetails(orders[0].id, function(details) {
// deeply nested and hard to follow
});
});
});
Promises improve this by giving you a clean chain. Instead of nesting functions inside functions, you move step by step with .then().
getUser(1)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details))
.catch(error => console.error(error));
A Promise is created using the built-in Promise constructor and always produces one of three possible states.
Every Promise exists in exactly one of three states at any given moment:
- Pending - The initial state. The operation has not finished yet.
- Fulfilled - The operation completed successfully and the Promise has a result value.
- Rejected - The operation failed and the Promise has a reason (an error).
Once a Promise moves from pending to either fulfilled or rejected, it is settled and its state can never change again.
// A Promise starts as "pending"
const myPromise = new Promise((resolve, reject) => {
// The executor runs immediately; async work can start here
});
// After calling resolve() it becomes "fulfilled"
// After calling reject() it becomes "rejected"
// Once settled, the state is locked forever
const p = new Promise((resolve, reject) => {
resolve("Success!"); // moves to fulfilled
reject("This is ignored"); // too late, already settled
});
p.then(value => console.log(value)); // "Success!"
You create a Promise using the new Promise() constructor. It takes a single function called the executor function. The executor runs immediately when the constructor is called and receives two arguments: resolve and reject. Calling resolve(value) fulfills the Promise, while calling reject(reason) rejects it. Only the first settlement takes effect; later calls are ignored.
const myPromise = new Promise(function(resolve, reject) {
// Do async work here
const success = true;
if (success) {
resolve("The operation worked!");
} else {
reject(new Error("Something went wrong."));
}
});
In real-world code, you often wrap async operations like setTimeout, network requests, or file reads inside a Promise.
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: 1, status: "shipped" }); // simulate a server response
}, 1000);
});
}
fetchData().then(data => console.log(data));
// After 1 second: { id: 1, status: "shipped" }
You can also use the shorthand static methods Promise.resolve() and Promise.reject() to create already-settled Promises instantly. These are useful in tests or when you want to return a value that already looks like a Promise.
const resolved = Promise.resolve(42);
resolved.then(v => console.log(v)); // 42
const rejected = Promise.reject(new Error("Oops!"));
rejected.catch(e => console.error(e.message)); // Oops!
Once you have a Promise, you react to its result using:
.then(onFulfilled, onRejected)- handles fulfillment and can also receive a rejection handler. The fulfillment handler receives the fulfilled value..catch(onRejected)- runs when the Promise is rejected. Receives the rejection reason (usually an error)..finally(onFinally)- runs after the Promise settles regardless of success or failure. Useful for cleanup.
function divide(a, b) {
return new Promise((resolve, reject) => {
if (b === 0) {
reject(new Error("Cannot divide by zero"));
} else {
resolve(a / b);
}
});
}
divide(10, 2)
.then(result => console.log("Result:", result)) // Result: 5
.catch(error => console.error("Error:", error.message));
divide(10, 0)
.then(result => console.log("Result:", result)) // skipped
.catch(error => console.error("Error:", error.message)); // Error: Cannot divide by zero
function loadData() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("Data loaded"), 500);
});
}
loadData()
.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => console.log("Loading complete")); // always runs
An important detail: .then() itself returns a new Promise. This is what makes chaining possible.
Because .then() always returns a new Promise, you can chain multiple .then() calls to run asynchronous steps one after another. Returning a normal value from a callback fulfills the next Promise with that value. Returning a Promise makes the chain wait for it, while throwing an error causes the next Promise in the chain to reject.
function getOrder(id) {
return Promise.resolve({ id: id, status: "shipped" });
}
function getTracking(order) {
return Promise.resolve({ orderId: order.id, location: "Distribution Center" });
}
function displayResult(data) {
console.log(`Order ${data.orderId} is at ${data.location}`);
}
getOrder(1)
.then(order => getTracking(order)) // returns a new Promise
.then(data => displayResult(data)) // runs after getTracking settles
.catch(error => console.error(error));
// Order 1 is at Distribution Center
You only need one .catch() at the end of the chain. If any step throws an error or returns a rejected Promise, the chain skips all remaining .then() handlers and jumps straight to .catch().
Promise.resolve(10)
.then(n => n * 2) // 20
.then(n => { throw new Error("Oops!"); }) // triggers error
.then(n => n + 1) // SKIPPED
.catch(err => console.error(err.message)); // "Oops!"
Keep your chains flat. Avoid returning a .then() inside another .then() callback unnecessarily as that brings back the nesting problem Promises were designed to solve. For longer or more branching workflows, async/await can make the control flow easier to read; choose the style that keeps the dependencies and error handling clear.
Practical rule: Use Promise.all() when independent operations can be started concurrently, and use async/await when it makes a longer or branching workflow easier to read. The important part is to preserve the dependency order and handle failures deliberately.
Promise.all() takes an iterable of Promises or values and returns a new Promise. That new Promise fulfills when all of the inputs fulfill, with results in input order. It rejects if any one of them rejects, using that rejection reason.
const p1 = Promise.resolve("User data");
const p2 = Promise.resolve("Order data");
const p3 = Promise.resolve("Product data");
Promise.all([p1, p2, p3])
.then(results => {
console.log(results);
// ["User data", "Order data", "Product data"]
})
.catch(error => console.error("One failed:", error));
The resolved value is an array of results in the same order as the input array, regardless of which Promise resolved first. This makes it easy to match results to their source.
const fast = Promise.resolve("Fast");
const failing = Promise.reject(new Error("Network error"));
const slow = Promise.resolve("Slow");
Promise.all([fast, failing, slow])
.then(results => console.log(results)) // SKIPPED
.catch(error => console.error(error.message)); // "Network error"
Use Promise.all() when all operations must succeed and you want to start independent asynchronous operations concurrently rather than waiting for each one to complete before starting the next.
If independent asynchronous operations are awaited one after another, each operation may delay the start of the next one. Starting independent operations first and using Promise.all() allows them to run concurrently.
Promise.allSettled() takes an iterable of Promises or values. Unlike Promise.all(), it always fulfills after every input settles, whether fulfilled or rejected, with an array of outcome objects describing each input.
Each result object has:
{ status: "fulfilled", value: ... }for successful Promises{ status: "rejected", reason: ... }for failed Promises
const p1 = Promise.resolve("Success A");
const p2 = Promise.reject(new Error("Failed B"));
const p3 = Promise.resolve("Success C");
Promise.allSettled([p1, p2, p3]).then(results => {
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("OK:", result.value);
} else {
console.log("FAIL:", result.reason.message);
}
});
});
// OK: Success A
// FAIL: Failed B
// OK: Success C
Use Promise.allSettled() when you want to start multiple independent operations concurrently and handle every result individually, even if some fail.
Promise.race() takes an iterable of Promises or values and returns a new Promise that settles as soon as the first input settles, whether fulfilled or rejected.
const slow = new Promise(resolve => setTimeout(() => resolve("Slow"), 2000));
const fast = new Promise(resolve => setTimeout(() => resolve("Fast"), 500));
const medium = new Promise(resolve => setTimeout(() => resolve("Medium"), 1000));
Promise.race([slow, fast, medium])
.then(winner => console.log(winner)); // "Fast" (resolves first)
A practical use case is implementing timeouts. You race your actual operation against a timeout Promise. If the timeout settles first, you treat the operation as failed.
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
const fetchSomething = new Promise(resolve =>
setTimeout(() => resolve("Data"), 3000)
);
withTimeout(fetchSomething, 1000)
.then(data => console.log(data))
.catch(err => console.error(err.message)); // "Timed out after 1000ms"
Note: Promise.race() does not cancel the losing Promise. Other operations that have already started can continue to completion and their results are ignored once the winner settles. If the original operation should be cancelled after a timeout, use an API that supports cancellation, such as AbortController with fetch().
Promise.any() fulfills as soon as the first input fulfills, ignoring rejections while it waits. It rejects with an AggregateError only if every input in the iterable rejects.
Here is a quick reference for everything covered in this tutorial:
| Concept | What it does |
|---|---|
new Promise(executor) |
Creates a new Promise; executor receives resolve and reject |
| Pending | Initial state; the async work is still in progress |
| Fulfilled | Operation succeeded; resolve(value) was called |
| Rejected | Operation failed; reject(reason) was called |
.then() |
Handles fulfillment and can handle rejection; returns a new Promise (enables chaining) |
.catch() |
Handles a rejected Promise; catches errors from the whole chain |
.finally() |
Runs after the Promise settles, regardless of outcome |
Promise.resolve(v) |
Returns an already-fulfilled Promise with value v |
Promise.reject(r) |
Returns an already-rejected Promise with reason r |
Promise.all(arr) |
Fulfills when all input Promises fulfill; rejects if any rejects |
Promise.allSettled(arr) |
Always fulfills; returns an array of outcome objects for every Promise |
Promise.race(arr) |
Settles as soon as the first input Promise settles |
Promise.any(arr) |
Fulfills with the first successful result; rejects only if all reject |
- What is a Promise in JavaScript?
- What is the difference between
Promise.all()andPromise.allSettled()? - What does
.then()return? - How do you handle errors in a Promise chain?
- What is the difference between
Promise.race()andPromise.any()? - What is callback hell and how do Promises solve it?
- When would you use
Promise.allSettled()overPromise.all()?
Videos for this topic will be added soon.