HardMicrosoftGoogleAmazon

Custom Promise

Prompt

Implement a custom Promise-like class called MyPromise that mimics the core functionality of JavaScript's native Promise. Your implementation should support:

  • Creating a new promise with an executor function
  • Resolving and rejecting promises
  • Chaining with .then() and .catch()
  • Static methods MyPromise.resolve() and MyPromise.reject()

Playground

Hint 1

A promise has three states: pending, fulfilled, and rejected. It starts as pending and can move to fulfilled (with a value) or rejected (with a reason), but never back. Store the state, value, and reason as instance properties.

Hint 2

When .then() is called on a promise that's still pending, the callback can't run yet because there's no value. Store it in an array (this.onFulfilledCallbacks). Later, when resolve() is called, loop through the array and run all stored callbacks.

Hint 3

.then() must return a new MyPromise to support chaining. Inside this new promise, run the callback from the previous .then(), and resolve the new promise with whatever that callback returns. Wrap it in setTimeout(fn, 0) to keep it async.

Hint 4

.catch(fn) is just .then(null, fn). And if a callback throws an error, the new promise should reject with that error. Wrap every callback execution in a try/catch.

Solution

Explanation

This is the hardest question in the Promises section. But every piece of this solution exists for a clear reason. Let's go through it piece by piece so nothing feels like magic.

A promise is just a state machine

At any point, a promise is in one of three states:

  • Pending: We don't have a result yet. Still waiting.
  • Fulfilled: The operation succeeded. We have a value.
  • Rejected: The operation failed. We have a reason (error).

A promise starts as pending. It can move to fulfilled or rejected, but never back. And once it settles, it stays settled forever. If someone calls resolve() and then calls reject() right after, the second call does nothing.

In our code, that's this check:

const resolve = (value) => {
if (this.state === MyPromise.PENDING) {
// Only proceed if still pending
this.state = MyPromise.FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach((cb) => cb());
}
};

If this.state is already FULFILLED or REJECTED, the if check fails and nothing happens. This guarantees a promise only settles once.

The constructor and the executor

When you write new MyPromise((resolve, reject) => { ... }), the function you pass in is called the "executor." Our constructor immediately runs it and passes in resolve and reject:

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

The try/catch is important. If the executor throws an error (like calling a function that doesn't exist), we automatically reject the promise with that error. Without this, the error would crash the program instead of being caught by .catch().

The callback queue problem

This is the thing that confuses most people when they first implement promises.

When someone writes:

const p = new MyPromise((resolve) => {
setTimeout(() => resolve('done'), 1000);
});

p.then((value) => console.log(value));

Think about the timing. The setTimeout delays resolve by 1 second. But p.then(...) runs immediately, right after the promise is created. At that moment, this.state is still PENDING and this.value is still null. We don't have the value yet, so we can't run the callback.

What do we do? We save the callback for later. We push it into this.onFulfilledCallbacks. It just sits there, waiting.

One second later, resolve('done') fires. Inside resolve, after updating the state and value, we run through the callback queue:

this.onFulfilledCallbacks.forEach((cb) => cb());

Now every .then() callback that was waiting gets called with the resolved value.

But what about this case?

const p = MyPromise.resolve(42);
p.then((value) => console.log(value));

Here, the promise is already fulfilled when .then() is called. We don't need the queue. We just run the callback directly. That's why .then() has three branches:

  • If the promise is fulfilled: run the callback now
  • If the promise is rejected: run the error handler now
  • If the promise is pending: store the callback for later

Why .then() returns a new promise

This is what makes chaining work. Every call to .then() creates and returns a brand new promise. That new promise resolves with whatever the callback returns.

MyPromise.resolve(1)
.then((v) => v + 1) // returns a NEW promise that resolves with 2
.then((v) => v * 3) // returns a NEW promise that resolves with 6
.then((v) => console.log(v)); // logs 6

Each .then() in the chain is a separate promise. The value flows from one to the next like a pipeline.

If a callback throws an error, the new promise rejects with that error. That rejected promise skips over any .then() calls until it finds a .catch():

MyPromise.resolve(1)
.then(() => {
throw new Error('oops');
}) // this promise REJECTS
.then(() => console.log('skipped')) // skipped entirely
.catch((e) => console.log(e.message)); // catches: "oops"

Why setTimeout in the handlers?

You'll notice the handlers are wrapped in setTimeout(() => { ... }, 0). This ensures callbacks always run asynchronously, even if the promise is already settled.

Native promises use the microtask queue for this, but setTimeout is the simplest way to simulate it in a polyfill. Without it, some callbacks would run synchronously and others asynchronously, which creates inconsistent behavior that's really hard to debug.

The resolvePromise helper

This is the most complex part, but it handles an important case: what if a .then() callback returns another promise?

promise.then(() => {
return new MyPromise((resolve) => resolve(42));
});

Without resolvePromise, the next .then() would receive the entire MyPromise object as its value, not 42. We don't want that. We want to "unwrap" the inner promise and wait for it to settle.

That's what resolvePromise does. It checks: is the result a "thenable" (an object or function with a .then() method)? If yes, it calls that .then() and waits for the inner promise to resolve, then uses that value to resolve the outer promise. If the result is a regular value (a number, string, etc.), it just resolves immediately.

It also handles two edge cases:

  1. Circular references: If a promise somehow resolves to itself, that would cause an infinite loop. We detect this and reject with a TypeError.
  2. The called flag: Some badly-written thenables might call both resolve and reject, or call one of them multiple times. The called flag ensures we only handle the first call.

.catch() is just sugar

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

.catch(fn) is literally just .then(null, fn). When there's no onFulfilled handler, the default is (value) => value, which passes the value through unchanged. And when there's no onRejected handler, the default is (reason) => { throw reason }, which re-throws the error so it propagates down.

This is how errors skip past .then() calls and fall through to the next .catch() in the chain. Each .then() without a rejection handler just passes the error along until something catches it.

Static methods

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

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

These are convenience methods for creating pre-settled promises. MyPromise.resolve(42) creates a promise that's already fulfilled with 42. MyPromise.reject('error') creates one that's already rejected. They're useful for converting plain values into promises or for testing.

Interviewer Criteria

Promise Implementation

  • Did I correctly implement the three promise states (pending, fulfilled, rejected)?

  • Does my implementation ensure that a promise can only transition from pending to fulfilled/rejected once?

  • Have I properly implemented the executor function that takes resolve and reject callbacks?

  • Did I ensure that errors thrown in the executor are caught and turn into rejections?

Promise Methods

  • Is my .then() method correctly implemented to handle both fulfilled and rejected states?

  • Does my .then() method return a new promise for proper chaining?

  • Have I implemented .catch() as a shorthand for .then(null, onRejected)?

  • Did I correctly implement the static methods MyPromise.resolve() and MyPromise.reject()?

Asynchronous Behavior

  • Did I ensure callbacks are executed asynchronously (using setTimeout or similar)?

  • Have I properly handled the case where a promise resolves to another promise?

  • Did I implement proper error propagation through promise chains?

  • Have I handled the case where a promise might resolve to itself (circular reference)?

Time Checkpoints

  • 10:00 AM

    Interview starts

  • 10:03 AM

    Prompt given by the interviewer

  • 10:05 AM

    Candidate reads the prompt, asks clarifying questions, and starts coding

  • 10:10 AM

    Define MyPromise class with constructor and basic state management

  • 10:15 AM

    Implement resolve and reject functions in the constructor

  • 10:20 AM

    Implement basic .then() method without chaining

  • 10:30 AM

    Enhance .then() to support promise chaining

  • 10:40 AM

    Implement .catch() method

  • 10:45 AM

    Add static resolve and reject methods

  • 10:50 AM

    Handle edge cases like thenable objects and circular references

  • 10:55 AM

    Test implementation with various scenarios

  • 11:00 AM

    Interview ends