jsguides

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

ParameterTypeDefaultDescription
executorfunctionA function with resolve and reject parameters that initiates the async operation
resolvefunctionCallback to resolve the promise with a value
rejectfunctionCallback to reject the promise with a reason (error)
promisesarrayAn iterable of Promises/values to wait for

Static Methods

MethodReturn TypeDescription
Promise.resolve(value)PromiseReturns a resolved Promise
Promise.reject(reason)PromiseReturns a rejected Promise
Promise.all(iterable)PromiseResolves when all input Promises resolve, rejects if any reject
Promise.allSettled(iterable)PromiseResolves when all input Promises settle (never rejects)
Promise.race(iterable)PromiseResolves/rejects as soon as the first input Promise settles
Promise.any(iterable)PromiseResolves when any input Promise resolves, rejects if all reject

Instance Methods

MethodReturn TypeDescription
promise.then(onFulfilled, onRejected)PromiseAttaches callbacks for fulfillment and rejection
promise.catch(onRejected)PromiseAttaches a callback for rejection
promise.finally(onFinally)PromiseAttaches 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