Skip to content
{JS World}

Promises

A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. It exists in one of three states: pending (initial), fulfilled (succeeded), or rejected (failed) — and once settled (fulfilled/rejected), its state never changes again.

const promise = new Promise((resolve, reject) => {
  const success = true;
  setTimeout(() => {
    if (success) {
      resolve("Data loaded");
    } else {
      reject(new Error("Failed to load"));
    }
  }, 1000);
});

promise
  .then((result) => console.log(result)) // runs if resolved
  .catch((error) => console.error(error)) // runs if rejected
  .finally(() => console.log("Done")); // always runs, regardless of outcome

Chaining

Each .then() returns a new promise, which lets you chain async steps sequentially instead of nesting callbacks (“callback hell”).

fetch("/api/user/1")
  .then((res) => res.json())
  .then((user) => fetch(`/api/posts?user=${user.id}`))
  .then((res) => res.json())
  .then((posts) => console.log(posts))
  .catch((err) => console.error("Something in the chain failed:", err));
// a single .catch() at the end handles errors from any step above it

async/await

Syntactic sugar over promises — lets asynchronous code read like synchronous code. An async function always returns a promise, and await pauses execution until that promise settles.

async function loadUserPosts(id) {
  try {
    const userRes = await fetch(`/api/user/${id}`);
    const user = await userRes.json();

    const postsRes = await fetch(`/api/posts?user=${user.id}`);
    const posts = await postsRes.json();

    return posts;
  } catch (err) {
    console.error("Something in the chain failed:", err);
  }
}

Combinators

  • Promise.all()

    Runs promises in parallel; resolves when all succeed, or rejects as soon as any one fails (fail-fast).

    Promise.all([fetch("/a"), fetch("/b"), fetch("/c")])
      .then((results) => console.log("All succeeded:", results))
      .catch((err) => console.error("At least one failed:", err));
  • Promise.allSettled()

    Runs promises in parallel; always resolves once all have settled, giving you the outcome of each — never short-circuits on failure.

    Promise.allSettled([
      Promise.resolve(1),
      Promise.reject("error"),
      Promise.resolve(3)
    ]).then((results) => console.log(results));
    // [
    //   { status: "fulfilled", value: 1 },
    //   { status: "rejected", reason: "error" },
    //   { status: "fulfilled", value: 3 }
    // ]
  • Promise.race()

    Settles as soon as the first promise settles (whether fulfilled or rejected) — useful for timeouts.

    function withTimeout(promise, ms) {
      const timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error("Timed out")), ms)
      );
      return Promise.race([promise, timeout]);
    }
    
    withTimeout(fetch("/slow-endpoint"), 3000).catch((err) =>
      console.error(err.message)
    );
  • Promise.any()

    Resolves as soon as the first promise fulfills; only rejects if all of them reject (with an AggregateError).

    Promise.any([
      Promise.reject("fail 1"),
      Promise.resolve("fail 2, but this one wins"),
      Promise.reject("fail 3")
    ]).then(
      (result) => console.log(result) // "fail 2, but this one wins"
    );

Common pitfall — sequential vs parallel

// Sequential (slower) — each await blocks the next one from starting
async function sequential() {
  const a = await fetch("/a"); // waits, then...
  const b = await fetch("/b"); // waits, then...
  return [a, b];
}

// Parallel (faster) — both requests fire immediately, then we wait together
async function parallel() {
  const [a, b] = await Promise.all([fetch("/a"), fetch("/b")]);
  return [a, b];
}