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 the following features:
- Creating a new promise with an executor function
- Resolving and rejecting promises
- Chaining promises with
.then()
and.catch()
- Static methods like
MyPromise.resolve()
andMyPromise.reject()
Requirements
- Create a
MyPromise
class that takes an executor function withresolve
andreject
parameters - Implement the
.then(onFulfilled, onRejected)
method for chaining promises - Implement the
.catch(onRejected)
method for error handling - Implement static methods
MyPromise.resolve(value)
andMyPromise.reject(reason)
- Handle asynchronous operations correctly
- Ensure proper error propagation through promise chains
Example
// Creating a promise
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
// Chaining with .then()
promise
.then(value => {
console.log(value); // 'Success!'
return 'Next value';
})
.then(value => {
console.log(value); // 'Next value'
})
.catch(error => {
console.error(error);
});
// Using static methods
MyPromise.resolve('Direct resolve')
.then(value => console.log(value)); // 'Direct resolve'
MyPromise.reject('Error occurred')
.catch(error => console.error(error)); // 'Error occurred'
Playground
Start by defining the possible states of a promise: PENDING
, FULFILLED
, and REJECTED
. A promise begins in the PENDING
state and can transition to either FULFILLED
(with a value) or REJECTED
(with a reason) state. Once a promise transitions to FULFILLED
or REJECTED
, it cannot change state again.
class MyPromise {
static PENDING = 'pending';
static FULFILLED = 'fulfilled';
static REJECTED = 'rejected';
constructor(executor) {
this.state = MyPromise.PENDING;
this.value = null;
this.reason = null;
// Define resolve and reject functions
// ...
}
}
For the .then()
method, remember that it should:
- Return a new promise
- Handle both synchronous and asynchronous resolution
- Pass the result of the previous promise to the callback
- Propagate errors if no error handler is provided
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
// Handle different states and callbacks
// ...
});
}
Don't forget to handle errors that might occur during the execution of callbacks. If an error is thrown in a .then()
callback, the returned promise should be rejected with that error.
try {
const result = onFulfilled(this.value);
resolve(result);
} catch (error) {
reject(error);
}
Solution
Explanation
Let's break down how our custom Promise implementation works, exploring the core concepts of JavaScript promises and asynchronous programming.
Promise States and Structure
At the heart of our implementation is the concept of promise states:
static PENDING = 'pending';
static FULFILLED = 'fulfilled';
static REJECTED = 'rejected';
A promise can be in one of three states:
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
Our MyPromise
class maintains these states along with:
value
: The resolved value (when fulfilled)reason
: The rejection reason (when rejected)onFulfilledCallbacks
: Array of callbacks to execute when the promise is fulfilledonRejectedCallbacks
: Array of callbacks to execute when the promise is rejected
The Promise Constructor
The constructor takes an executor function and immediately executes it:
constructor(executor) {
this.state = MyPromise.PENDING;
this.value = null;
this.reason = null;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === MyPromise.PENDING) {
this.state = MyPromise.FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback());
}
};
const reject = (reason) => {
if (this.state === MyPromise.PENDING) {
this.state = MyPromise.REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(callback => callback());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
The constructor:
- Initializes the promise state and values
- Defines
resolve
andreject
functions that will be passed to the executor - Calls the executor function, passing these functions as arguments
- Catches any errors thrown in the executor and rejects the promise
The resolve
and reject
functions:
- Check if the promise is still pending (a promise can only transition once)
- Update the state and store the value/reason
- Execute any registered callbacks
The .then() Method
The .then()
method is where the real magic happens:
then(onFulfilled, onRejected) {
// Handle case where callbacks aren't functions
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
// Create a new promise for chaining
const promise2 = new MyPromise((resolve, reject) => {
// Handler implementations...
});
return promise2;
}
This method:
- Normalizes the callbacks (providing defaults if they're not functions)
- Creates and returns a new promise for chaining
- Sets up handlers for both fulfilled and rejected states
The handlers are wrapped in setTimeout
to ensure asynchronous execution, mimicking the microtask behavior of native promises:
const fulfilledHandler = () => {
setTimeout(() => {
try {
const result = onFulfilled(this.value);
this.resolvePromise(promise2, result, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
};
Each handler:
- Calls the appropriate callback with the value/reason
- Resolves or rejects the new promise based on the result
- Catches any errors and rejects the new promise
The resolvePromise Method
This helper method handles the complex logic of resolving a promise with a value that might itself be a promise:
resolvePromise(promise, result, resolve, reject) {
// Handle circular references
if (promise === result) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// Handle if result is a promise-like object
if (result && (typeof result === 'object' || typeof result === 'function')) {
let called = false;
try {
const then = result.then;
if (typeof then === 'function') {
// If result is a thenable, call its then method
then.call(
result,
value => {
if (!called) {
called = true;
this.resolvePromise(promise, value, resolve, reject);
}
},
reason => {
if (!called) {
called = true;
reject(reason);
}
}
);
} else {
resolve(result);
}
} catch (error) {
if (!called) {
called = true;
reject(error);
}
}
} else {
resolve(result);
}
}
This method:
- Checks for circular references (a promise that resolves to itself)
- Determines if the result is a "thenable" (has a
.then()
method) - If it's a thenable, calls its
.then()
method to resolve the promise - Otherwise, resolves the promise with the value directly
- Uses a
called
flag to ensure the promise is only resolved or rejected once
The .catch() Method
The .catch()
method is a convenient shorthand for .then(null, onRejected)
:
catch(onRejected) {
return this.then(null, onRejected);
}
This allows for more readable error handling in promise chains.
Static Methods
Our implementation includes two static methods:
static resolve(value) {
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
These methods create pre-resolved or pre-rejected promises, which is useful for converting values into promises or creating promises that immediately fail.
Putting It All Together
When you use our MyPromise
implementation:
- You create a new promise with an executor function that calls
resolve
orreject
- You chain promises with
.then()
and handle errors with.catch()
- Each
.then()
returns a new promise that resolves with the return value of the callback - Errors propagate down the chain until caught by a
.catch()
This creates a powerful pattern for handling asynchronous operations in a clean, sequential manner, avoiding the "callback hell" that plagued earlier JavaScript code.
Key Challenges in Implementation
Implementing a Promise-like class involves several tricky aspects:
- Proper State Transitions: Ensuring a promise can only transition from pending to fulfilled/rejected once
- Asynchronous Execution: Using
setTimeout
to simulate the microtask queue - Error Handling: Catching and propagating errors through the promise chain
- Thenable Resolution: Handling values that are themselves promise-like
- Avoiding Circular References: Detecting and rejecting promises that resolve to themselves
Our implementation addresses all these challenges, creating a robust Promise implementation that closely mimics the behavior of native JavaScript Promises.
Common Pitfalls
Common Pitfalls
When implementing a custom Promise, there are several common mistakes to avoid:
-
Synchronous Execution: Native Promises execute callbacks asynchronously, even if the promise is already resolved. Forgetting to use
setTimeout
or similar can lead to unexpected behavior. -
Missing Error Handling: Errors thrown in the executor or callbacks should be caught and turn into rejections. Without proper error handling, exceptions can crash your application.
-
Ignoring Thenable Objects: A proper Promise implementation should handle "thenable" objects (objects with a
.then()
method) by treating them as promises. This is crucial for interoperability with other Promise implementations. -
Multiple Resolutions: Once a promise transitions from pending to fulfilled or rejected, it should never change state again. Allowing multiple resolutions breaks the Promise contract.
-
Circular Promise Chains: If a promise resolves to itself or creates a circular chain, it should be rejected with a TypeError. Without this check, you can create infinite recursion.
Code Quality
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)?
Edge Cases
Did I handle the case where
.then()
is called with non-function arguments?Have I ensured that a promise can be resolved/rejected with any value, including undefined?
Did I properly handle "thenable" objects (objects with a
.then()
method)?Have I ensured that callbacks are only called once, even if resolve/reject is called multiple times?
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 ✅