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()andMyPromise.reject()
Playground
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.
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.
.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.
.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 6Each .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:
- Circular references: If a promise somehow resolves to itself, that would cause an infinite loop. We detect this and reject with a
TypeError. - The
calledflag: Some badly-written thenables might call both resolve and reject, or call one of them multiple times. Thecalledflag 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()andMyPromise.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