JavaScriptPromises

Promises

A Promise is an object that represents a value you do not have yet — the eventual result of an asynchronous operation. Instead of passing a callback to be called later, the async function returns a promise, and you attach handlers to it with .then, .catch and .finally. Promises turn nested callbacks into chains that read top-to-bottom.

The three states

A promise is always in one of these states, and only transitions once:

  • Pending — the operation is in progress. The value is not known yet.

  • Fulfilled — the operation succeeded with a value.

  • Rejected — the operation failed with a reason (usually an Error).

Once a promise is fulfilled or rejected, it is settled — its state never changes again, and its value/reason is fixed forever.

Creating a promise

Most code consumes promises returned by libraries (fetch, fs/promises, etc.) rather than creating them. But you create one with the Promise constructor when wrapping a callback-based API.

JS
const wait = (ms) =>
  new Promise((resolve, reject) => {
    if (ms < 0) {
      reject(new Error("ms must be non-negative"));
      return;
    }
    setTimeout(() => resolve("done"), ms);
  });

wait(500).then((result) => console.log(result));   // "done", after 500ms
Don't wrap promises in promises
If a function already returns a promise, just return it. `new Promise((resolve) => fetch(url).then(resolve))` is an anti-pattern called the **promise constructor anti-pattern**. Use `return fetch(url)` instead.
Promise.resolve and Promise.reject

Two static shortcuts for creating already-settled promises. Useful for tests and for "lifting" a sync value into a promise chain.

JS
Promise.resolve(42).then((v) => console.log(v));    // 42

Promise.reject(new Error("nope"))
  .catch((err) => console.log(err.message));        // "nope"

// Passing an existing promise to Promise.resolve returns it unchanged.
const p = fetch("/api");
console.log(Promise.resolve(p) === p);              // true
.then, .catch, .finally

.then takes up to two functions: one for fulfilled, one for rejected. In practice most code passes only the fulfilled handler and uses .catch for errors — it reads better.

JS
fetch("/api/user")
  .then((response) => response.json())
  .then((user) => console.log(user))
  .catch((err) => console.error("failed:", err))
  .finally(() => console.log("done either way"));
  • .then(onFulfilled, onRejected) — handle either outcome.

  • .catch(onRejected) — shorthand for .then(undefined, onRejected).

  • .finally(onSettled) — run cleanup regardless of outcome. The value/error passes through.

Chaining: every handler returns a new promise

The big insight: .then returns a new promise whose value is whatever its handler returned. If the handler returns a promise, the chain waits for it. That is what makes long pipelines flat.

JS
Promise.resolve(2)
  .then((n) => n + 1)             // 3
  .then((n) => Promise.resolve(n * 10))  // waits, then 30
  .then((n) => console.log(n));   // 30

Compare to the old "pyramid of doom":

Callback version

JS
getUser(id, (err, user) => {
  if (err) return done(err);
  getPosts(user.id, (err, posts) => {
    if (err) return done(err);
    renderPosts(posts, (err) => {
      if (err) return done(err);
      done(null);
    });
  });
});

Promise version

JS
getUser(id)
  .then((user) => getPosts(user.id))
  .then((posts) => renderPosts(posts))
  .catch((err) => done(err));
Error propagation

A rejection skips every .then until it hits a .catch. Inside a .then handler, throwing is equivalent to returning a rejected promise.

JS
Promise.resolve("input")
  .then((s) => {
    if (s.length < 5) throw new Error("too short");
    return s.toUpperCase();
  })
  .then((s) => console.log("got", s))
  .catch((err) => console.log("caught:", err.message));
caught: too short
Common pitfalls
  • Forgetting to return inside .then. If you do not return, the next handler receives undefined and the chain does not wait for nested work.

  • Catch in the wrong place. A .catch only handles rejections from upstream steps. Put one at the end of every chain to be safe.

  • Unhandled rejection. A rejection with no .catch triggers an unhandledrejection event in browsers (and crashes Node by default).

  • Constructor anti-pattern. Wrapping an existing promise in a new one — see above.

Forgotten return

JS
function loadUser(id) {
  return Promise.resolve({ id, name: "Ada" });
}

// BUG: loadUser is fired, but its promise is dropped.
function badChain(id) {
  return Promise.resolve()
    .then(() => {
      loadUser(id);    // missing return
    })
    .then((user) => console.log(user));  // user is undefined
}

// FIX: return the inner promise.
function goodChain(id) {
  return Promise.resolve()
    .then(() => loadUser(id))
    .then((user) => console.log(user));
}
Promises and microtasks

.then handlers run as microtasks — sooner than setTimeout(fn, 0), but never synchronously. Even Promise.resolve(1).then(...) is not synchronous: the handler is scheduled, not called immediately.

JS
console.log("a");
Promise.resolve().then(() => console.log("b"));
console.log("c");
a
c
b
When to reach for async/await
Promise chains read top-to-bottom, but every `.then` is still a callback. `async`/`await` lets you write the same logic with `const x = await foo()` and normal `try/catch`. We cover that on its own page.