JavaScript Interview Prep
Asynchronous JavaScript

Promises

The Receipt That Runs Your Future Code

LinkedIn Hook

A Promise is the single most important data structure in modern JavaScript — and most developers who use async/await every day can't build one from scratch.

That matters because async/await is just syntactic sugar over Promises. The Promise is the actual object doing the work. If you don't understand the three states (pending, fulfilled, rejected), the fact that a Promise can only settle once, and how .then() returns a brand-new Promise that lets you chain, you can't reason about any async bug that gets interesting.

In this lesson you'll learn what a Promise is, the lifecycle it goes through, how to convert nested callback hell into a flat Promise chain, the difference between .then(), .catch(), and .finally(), and — for the truly curious — how to build a minimal Promise implementation from scratch.

If you've ever been asked "can you implement Promise?" in an interview and frozen up — this lesson is the fix.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #Promises #AsyncJS #Frontend #CodingInterview #WebDevelopment


Promises thumbnail


What You'll Learn

  • What a Promise is, the three states it can be in, and the one-way settlement rule
  • How to chain .then(), .catch(), and .finally() to replace nested callbacks with flat code
  • How to build a minimal Promise class from scratch to understand it from the inside out

What Is a Promise?

Think of a Promise like ordering food at a restaurant. When you order, the waiter gives you a receipt (the Promise). The food isn't ready yet — your receipt is in a pending state. Eventually one of two things happens: the kitchen successfully prepares your meal (fulfilled), or they're out of ingredients and can't make it (rejected). Either way, the receipt lets you plan what to do next.


Promise States

StateDescriptionSettled?
pendingInitial state, operation in progressNo
fulfilledOperation completed successfullyYes
rejectedOperation failedYes

A Promise can only transition once: pending -> fulfilled, or pending -> rejected. Once settled, it's immutable. Additional calls to resolve() or reject() are silently ignored.


Creating a Promise

const myPromise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve("Operation completed!");  // -> fulfilled
  } else {
    reject(new Error("Something went wrong"));  // -> rejected
  }
});

myPromise
  .then((result) => console.log(result))   // "Operation completed!"
  .catch((error) => console.error(error));

Callback Hell -> Promise Chain (The Evolution)

Here's the same nested callback hell from the previous lesson rewritten with Promises:

// Step 1: Promisify the functions
function getUser(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id: userId, name: "Rakibul" });
    }, 500);
  });
}

function getOrders(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([{ id: 101, item: "Laptop" }, { id: 102, item: "Phone" }]);
    }, 500);
  });
}

function getOrderDetails(orderId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id: orderId, trackingId: "TRK-789", status: "shipped" });
    }, 500);
  });
}

// Step 2: Flat Promise chain — no nesting!
getUser(1)
  .then((user) => {
    console.log("User:", user.name);
    return getOrders(user.id);
  })
  .then((orders) => {
    console.log("First order:", orders[0].item);
    return getOrderDetails(orders[0].id);
  })
  .then((details) => {
    console.log("Tracking:", details.trackingId);
    console.log("Status:", details.status);
  })
  .catch((error) => {
    // ONE catch handles errors from ANY step above
    console.error("Something failed:", error.message);
  })
  .finally(() => {
    console.log("Done — cleanup here (loading spinner off, etc.)");
  });

Key improvements over callbacks:

  • Flat structure — no pyramid, reads top to bottom.
  • Single error handler.catch() at the end catches any failure in the chain.
  • Composable — each .then() returns a new Promise, so you can keep chaining.
  • .finally() — runs regardless of success or failure (great for cleanup).

Building a Custom Promise from Scratch (Simplified)

Understanding how Promise works internally is a powerful interview signal:

class SimplePromise {
  constructor(executor) {
    this.state = "pending";
    this.value = undefined;
    this.callbacks = [];

    const resolve = (value) => {
      if (this.state !== "pending") return; // can only settle once
      this.state = "fulfilled";
      this.value = value;
      this.callbacks.forEach((cb) => cb.onFulfilled(value));
    };

    const reject = (reason) => {
      if (this.state !== "pending") return;
      this.state = "rejected";
      this.value = reason;
      this.callbacks.forEach((cb) => cb.onRejected(reason));
    };

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  then(onFulfilled, onRejected) {
    return new SimplePromise((resolve, reject) => {
      const handle = () => {
        try {
          if (this.state === "fulfilled") {
            const result = onFulfilled
              ? onFulfilled(this.value)
              : this.value;
            resolve(result);
          }
          if (this.state === "rejected") {
            if (onRejected) {
              const result = onRejected(this.value);
              resolve(result);
            } else {
              reject(this.value);
            }
          }
        } catch (err) {
          reject(err);
        }
      };

      if (this.state === "pending") {
        this.callbacks.push({
          onFulfilled: () => handle(),
          onRejected: () => handle(),
        });
      } else {
        // already settled — schedule microtask
        queueMicrotask(handle);
      }
    });
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

// Test it
const p = new SimplePromise((resolve) => {
  setTimeout(() => resolve("It works!"), 1000);
});

p.then((val) => console.log(val)); // "It works!" after 1 second

Promises visual 1


Common Mistakes

  • Calling resolve() or reject() multiple times and expecting both to take effect — the second call is silently ignored because a Promise can only settle once.
  • Forgetting that a .then() callback that returns a value produces a brand-new Promise — mutating shared state inside .then() instead of returning a chained value breaks composition.
  • Treating .catch() as a try/catch — .catch() only catches rejections from preceding steps in the chain, so a .catch() placed before a later .then() won't see failures in that later step.

Interview Questions

Q: What are the three states of a Promise?

Pending (initial), fulfilled (resolved successfully), rejected (failed). Once a Promise settles (fulfilled or rejected), its state and value are immutable.

Q: What is the difference between .then(), .catch(), and .finally()?

.then(onFulfilled, onRejected) handles fulfillment (and optionally rejection). .catch(onRejected) is sugar for .then(null, onRejected) — handles only rejections. .finally(callback) runs after settlement regardless of outcome and receives no arguments — ideal for cleanup like hiding loading spinners.

Q: What happens if you return a value inside .then()?

It gets automatically wrapped in a resolved Promise, so the next .then() in the chain receives it. If you return a Promise, the chain waits for it to settle.

Q: Can a Promise be resolved more than once?

No. Once a Promise transitions from pending to fulfilled or rejected, it's settled permanently. Additional calls to resolve() or reject() are silently ignored.

Q: Can a Promise change from fulfilled to rejected?

No. Settlement is a one-way, one-time transition. Once fulfilled, a Promise stays fulfilled; once rejected, it stays rejected.

Q: What does .finally() do?

Runs after the Promise settles — fulfilled or rejected — without receiving the value or reason. It's the cleanup hook: close modals, stop spinners, release resources, regardless of outcome.


Quick Reference — Cheat Sheet

PROMISES — QUICK MAP

States:
  pending   -> fulfilled (resolve)
            -> rejected  (reject)
  Once settled, IMMUTABLE.

Chain shape:
  doSomething()
    .then(result => ...)    // on success
    .catch(error => ...)    // on failure (any step)
    .finally(() => ...)     // always runs

Key rules:
  - Each .then() returns a NEW Promise (chainable)
  - Returning a value inside .then() wraps it in Promise.resolve()
  - Returning a Promise inside .then() waits for it
  - .catch(h) == .then(null, h)
  - .finally() receives no arguments

Previous: Callbacks & Callback Hell Next: async/await — Syntactic Sugar Over Promises


This is Lesson 3.7 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page