Skip to content

Instantly share code, notes, and snippets.

@carefree-ladka
Last active January 22, 2026 13:04
Show Gist options
  • Select an option

  • Save carefree-ladka/2c48e89c0ac56a747a68b3b8384cdb8e to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/2c48e89c0ac56a747a68b3b8384cdb8e to your computer and use it in GitHub Desktop.
Promise Impl for Interviews
class MyPromise {
constructor(executor) {
this.fulfilledHandlers = [];
this.rejectedHandlers = [];
this.state = "pending";
this.value = undefined;
const resolve = (value) => {
if (this.state !== "pending") return;
// self-resolution check
if (value === this) {
reject(new TypeError("Chaining cycle detected for promise"));
return;
}
this.state = "fulfilled";
this.value = value;
queueMicrotask(() => {
this.fulfilledHandlers.forEach((cb) => cb(value));
});
};
const reject = (reason) => {
if (this.state !== "pending") return;
this.state = "rejected";
this.value = reason;
queueMicrotask(() => {
this.rejectedHandlers.forEach((cb) => cb(reason));
});
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const fulfilled = (v) => {
try {
const result = typeof onFulfilled === "function" ? onFulfilled(v) : v;
result instanceof MyPromise
? result.then(resolve, reject)
: resolve(result);
} catch (e) {
reject(e);
}
};
const rejected = (err) => {
try {
if (typeof onRejected === "function") {
const result = onRejected(err);
result instanceof MyPromise
? result.then(resolve, reject)
: resolve(result);
} else {
reject(err);
}
} catch (e) {
reject(e);
}
};
if (this.state === "fulfilled") {
queueMicrotask(() => fulfilled(this.value));
} else if (this.state === "rejected") {
queueMicrotask(() => rejected(this.value));
} else {
this.fulfilledHandlers.push(fulfilled);
this.rejectedHandlers.push(rejected);
}
});
}
catch(fn) {
return this.then(null, fn);
}
finally(callback) {
return this.then(
(value) => MyPromise.resolve(callback()).then(() => value),
(reason) =>
MyPromise.resolve(callback()).then(() => {
throw reason;
})
);
}
static resolve(value) {
if (value instanceof MyPromise) return value;
return new MyPromise((resolve) => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}
@carefree-ladka
Copy link
Author

MyPromise Implementation Guide

1️⃣ Class Definition & Constructor

class MyPromise {
  constructor(executor) {
    this.fulfilledHandlers = [];
    this.rejectedHandlers = [];
    this.state = "pending";
    this.value = undefined;

Explanation:

  • fulfilledHandlers → stores all .then callbacks for success
  • rejectedHandlers → stores all .then/.catch callbacks for failure
  • state → tracks the promise state: "pending" | "fulfilled" | "rejected"
  • value → holds either the resolved value or rejection reason

2️⃣ Resolve & Reject Functions

const resolve = (value) => {
  if (this.state !== "pending") return;

  queueMicrotask(() => {
    this.state = "fulfilled";
    this.value = value;
    this.fulfilledHandlers.forEach((cb) => cb(value));
  });
};

Explanation:

  • First, we check if the promise is still pending
  • ✅ Promises can only be resolved/rejected once
  • queueMicrotask ensures the callbacks run asynchronously, even if resolve is called synchronously
  • Updates state to "fulfilled" and stores the value
  • Calls all queued success handlers
const reject = (reason) => {
  if (this.state !== "pending") return;

  queueMicrotask(() => {
    this.state = "rejected";
    this.value = reason;
    this.rejectedHandlers.forEach((cb) => cb(reason));
  });
};

Explanation:

  • Same as resolve, but for failure
  • Calls all queued failure handlers in microtasks

3️⃣ Executor Try-Catch

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

Explanation:

  • The executor is called immediately, synchronously
  • If it throws an error, we reject the promise automatically
  • ✅ This matches native Promise behavior

4️⃣ then Method

then(onFulfilled, onRejected) {
  return new MyPromise((resolve, reject) => {

Explanation:

  • .then() always returns a new promise
  • This enables chaining
const fulfilled = (v) => {
  try {
    const result = typeof onFulfilled === "function" ? onFulfilled(v) : v;

    result instanceof MyPromise
      ? result.then(resolve, reject)
      : resolve(result);
  } catch (e) {
    reject(e);
  }
};

Explanation:

  • If onFulfilled is a function, call it with the value
  • If the callback returns a promise, we chain it using result.then(resolve, reject)
  • If it returns a normal value, resolve the new promise with it
  • Any errors thrown are caught and reject the new promise
const rejected = (err) => {
  try {
    if (typeof onRejected === "function") {
      const result = onRejected(err);
      result instanceof MyPromise
        ? result.then(resolve, reject)
        : resolve(result);
    } else {
      reject(err);
    }
  } catch (e) {
    reject(e);
  }
};

Explanation:

  • Works like fulfilled, but for errors
  • If no onRejected provided, propagate the rejection
  • Allows .catch() to work

5️⃣ State Checks in then

if (this.state === "fulfilled") {
  queueMicrotask(() => fulfilled(this.value));
} else if (this.state === "rejected") {
  queueMicrotask(() => rejected(this.value));
} else {
  this.fulfilledHandlers.push(fulfilled);
  this.rejectedHandlers.push(rejected);
}

Explanation:

  • Already settled: Schedule the handler in a microtask (async)
  • Pending: Store the handler for later, when promise resolves/rejects

6️⃣ catch Method

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

Explanation:

  • .catch(fn) is just shorthand for .then(null, fn)

7️⃣ finally Method

finally(callback) {
  return this.then(
    (value) => MyPromise.resolve(callback()).then(() => value),
    (reason) =>
      MyPromise.resolve(callback()).then(() => {
        throw reason;
      })
  );
}

Explanation:

  • Runs callback() after success or failure
  • Returns original value or reason
  • If callback returns a promise, waits for it (MyPromise.resolve)
  • If callback throws, it changes the chain to rejected

8️⃣ Static resolve & reject

static resolve(value) {
  if (value instanceof MyPromise) return value;
  return new MyPromise((resolve) => resolve(value));
}

static reject(reason) {
  return new MyPromise((_, reject) => reject(reason));
}

Explanation:

  • MyPromise.resolve(…) → wraps a value into a promise (or returns if already a promise)
  • MyPromise.reject(…) → creates a rejected promise immediately

Examples

Example 1 — Simple Resolve

MyPromise.resolve(10)
  .then((v) => {
    console.log(v); // 10
    return v * 2;
  })
  .then((v) => console.log(v)); // 20

Step-by-step:

  1. resolve(10) queues microtask
  2. First .then runs → logs 10 → returns 20
  3. Second .then runs → logs 20

Example 2 — Nested Promises

MyPromise.resolve(1)
  .then((v) => {
    console.log(v); // 1
    return MyPromise.resolve(2);
  })
  .then((v) => console.log(v)); // 2

Explanation:

  • Inner promise returned → then automatically waits for it
  • This is promise unwrapping

Example 3 — finally

MyPromise.resolve("done")
  .finally(() => console.log("cleanup"))
  .then(console.log);

Output:

cleanup
done
  • finally runs callback after fulfillment
  • Does not change the value

Example 4 — Rejection Handling

MyPromise.reject("error")
  .catch((e) => console.log(e)) // error
  .finally(() => console.log("always"));

Output:

error
always
  • .catch handles rejection
  • .finally always runs

✅ Summary

  • Microtasks: Ensure async execution even on synchronous resolve/reject
  • Chaining: .then returns a new MyPromise
  • Error handling: Try-catch inside callbacks propagates errors
  • Finally: Runs after success/failure, keeps original value/reason
  • Static helpers: resolve and reject create promises immediately

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment