Promise
The Promise object represents the eventual completion or failure of an asynchronous operation. Introduced in ES6, it provides a cleaner alternative to callback-based asynchronous code. A Promise is in one of three states: pending (initial state), fulfilled (operation completed successfully), or rejected (operation failed).
Syntax
new Promise(executor)
Promise.resolve(value)
Promise.reject(reason)
Promise.all(promises)
Promise.allSettled(promises)
Promise.race(promises)
Promise.any(promises)
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
executor | function | — | A function with resolve and reject parameters that initiates the async operation |
resolve | function | — | Callback to resolve the promise with a value |
reject | function | — | Callback to reject the promise with a reason (error) |
promises | array | — | An iterable of Promises/values to wait for |
Static Methods
| Method | Return Type | Description |
|---|---|---|
Promise.resolve(value) | Promise | Returns a resolved Promise |
Promise.reject(reason) | Promise | Returns a rejected Promise |
Promise.all(iterable) | Promise | Resolves when all input Promises resolve, rejects if any reject |
Promise.allSettled(iterable) | Promise | Resolves when all input Promises settle (never rejects) |
Promise.race(iterable) | Promise | Resolves/rejects as soon as the first input Promise settles |
Promise.any(iterable) | Promise | Resolves when any input Promise resolves, rejects if all reject |
Instance Methods
| Method | Return Type | Description |
|---|---|---|
promise.then(onFulfilled, onRejected) | Promise | Attaches callbacks for fulfillment and rejection |
promise.catch(onRejected) | Promise | Attaches a callback for rejection |
promise.finally(onFinally) | Promise | Attaches a callback regardless of outcome |
Examples
Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("Operation succeeded!");
} else {
reject(new Error("Operation failed"));
}
});
myPromise.then(result => {
console.log(result); // "Operation succeeded!"
}).catch(error => {
console.error(error.message);
});
The executor function receives resolve and reject as its arguments, and whichever you call first determines the promise’s settled state. After that first call, further calls to either function are silently ignored. This example wraps a synchronous condition, but the executor typically starts a timer, makes a network request, or reads a file.
Chaining Promises
fetchUserData(userId)
.then(user => fetchUserPosts(user.id))
.then(posts => {
console.log(`Found ${posts.length} posts`);
return posts;
})
.catch(error => {
console.error("Failed to fetch user data:", error);
});
Each .then() returns a new promise, so you can build a pipeline of transformations. A single .catch() at the end handles errors from any step. If any .then() callback throws or returns a rejected promise, execution skips straight to the catch handler, skipping any remaining .then() steps.
Using Promise.all()
const urls = [
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/comments"
];
const requests = urls.map(url => fetch(url).then(r => r.json()));
Promise.all(requests)
.then(([users, posts, comments]) => {
console.log("All data loaded:", { users, posts, comments });
})
.catch(error => {
console.error("One or more requests failed:", error);
});
Promise.all() is an all-or-nothing combinator. If every promise resolves, you get an array of results in the same order as the input promises. But if even one rejects, the entire Promise.all() rejects immediately with that error — any remaining results from other promises are discarded.
Using Promise.allSettled()
const results = await Promise.allSettled([
fetch("/api/users"),
fetch("/api/posts"),
fetch("/api/unknown")
]);
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`Request ${index} succeeded:`, result.value);
} else {
console.log(`Request ${index} failed:`, result.reason);
}
});
Unlike Promise.all(), allSettled() never rejects — it waits for every input promise to finish and reports each outcome individually. You get an array of objects with a status field set to either "fulfilled" or "rejected". This is the right choice when you need results from all requests regardless of failures.
Using Promise.race()
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
);
}
Promise.race([
fetch("https://slow-api.example.com/data"),
timeout(5000)
])
.then(data => console.log("Data received:", data))
.catch(error => console.error("Race finished:", error.message));
Promise.race() settles as soon as the first promise in the iterable settles, whether resolved or rejected. This makes it ideal for timeouts: race your main request against a timer that rejects after a cutoff. If the request finishes first, the timer is ignored — but if the timer fires first, the request is abandoned and the race rejects.
Common Patterns
Converting callback to a promise
function promisify(callbackBasedFn) {
return (...args) => new Promise((resolve, reject) => {
callbackBasedFn(...args, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
}
// Usage
const readFileAsync = promisify(fs.readFile);
const data = await readFileAsync("./package.json", "utf-8");
The promisify wrapper translates Node.js-style callbacks — where the last argument is (error, result) — into a promise-returning function. This pattern applies to any API that follows the error-first callback convention. Modern Node.js exposes promise-based versions of most core modules under node:fs/promises, but promisify still handles third-party libraries that only offer callback APIs.
Promise.withResolvers()
// Modern pattern (ES2024+)
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve("Done!"), 1000);
const result = await promise;
Promise.withResolvers() returns an object with the promise itself plus its resolve and reject functions, exposed as named properties. This avoids the awkward pattern of declaring variables outside the executor scope and assigning them inside. It is especially useful when the resolution trigger lives in a different part of the code, such as an event listener or a queue consumer.
Avoiding promise anti-patterns
// BAD: Creating promises unnecessarily
const bad = new Promise((resolve, reject) => {
resolve(await fetchData()); // Don't await inside executor!
});
// GOOD: await the promise directly
const good = await fetchData();
Promise states and transitions
A Promise can only move forward, never backward. From pending, it transitions to either fulfilled (with a resolved value) or rejected (with a rejection reason). Once a Promise settles — fulfilled or rejected — its state and value are permanently fixed. Calling resolve() or reject() a second time has no effect.
const p = new Promise((resolve, reject) => {
resolve("first");
resolve("second"); // ignored — already settled
reject("too late"); // also ignored
});
p.then(v => console.log(v)); // "first"
The immutability of settled promises is a key safety property. You can pass a resolved promise to multiple consumers, store it in a cache, or use it as a return value without worrying that someone will change its outcome. Once the promise is settled, every .then() attached to it will see the same value, whether the attachment happens before or after settlement.
The executor function
The executor function passed to new Promise() runs synchronously, immediately when the Promise is created. Only the async continuation (the .then() callbacks) runs later via the microtask queue.
console.log("before");
new Promise((resolve) => {
console.log("executor"); // runs synchronously
resolve("done");
});
Promise.resolve().then(() => console.log("microtask"));
console.log("after");
// Output: before, executor, after, microtask
The executor should not throw — any thrown error is caught and converts the Promise to rejected, but this only works at the top level of the executor, not inside asynchronous callbacks started within it.
Promise chaining and the microtask queue
Each .then() call returns a new Promise, which is why chaining works. The callback passed to .then() never runs synchronously — it is always scheduled as a microtask, meaning it runs after the current synchronous code completes but before the next macrotask (such as setTimeout callbacks). This guarantees predictable ordering even when Promises resolve immediately.
Promise.resolve(1)
.then(v => v + 1) // returns Promise<2>
.then(v => v * 3) // returns Promise<6>
.then(console.log); // logs 6
Why promise matters
Promise gives JavaScript a standard way to represent work that finishes later. That lets you write code that describes the desired result instead of wiring every step together with nested callbacks. The promise itself becomes a placeholder for eventual success or failure, and that makes asynchronous code much easier to compose.
The three-state model is the real mental model to keep in mind. A promise starts pending, then settles once. After that, it never changes again. That immutability is what makes chaining predictable and why a promise can be passed around safely without needing to know exactly when the operation completed.
Picking the right static method
The static helpers answer different questions. Use resolve() when you already have a value and want a promise wrapper. Use reject() when you want a promise that is already failed. Use all() when every input must succeed, allSettled() when you want every result, race() when the first settled result wins, and any() when one success is enough.
That choice matters because it shapes the failure behavior of the whole flow. Picking the right method early makes the code easier to read and keeps the error path aligned with the real business rule.
See Also
- fetch() — Make HTTP requests that return Promises
- Promise.resolve() — Create a resolved Promise