Build Your Own Promise: A JavaScript Asynchronous Foundation
The native Promise object in JavaScript is fundamental for handling asynchronous operations. Understanding its internal workings is crucial for mastering asynchronous programming. This challenge asks you to implement a custom Promise class from scratch, replicating the core behavior of the native Promise. This will deepen your understanding of callback management, state transitions, and asynchronous execution.
Problem Description
You are tasked with creating a MyPromise class in JavaScript that mimics the behavior of the built-in Promise. This class should handle asynchronous operations by maintaining a state (pending, fulfilled, or rejected) and executing callbacks based on that state.
Key Requirements:
-
Constructor (
MyPromise(executor)):- The constructor should accept an
executorfunction. - The
executorfunction is immediately invoked with two arguments:resolveandreject. resolve(value): Transitions the promise to the "fulfilled" state with the givenvalue.reject(reason): Transitions the promise to the "rejected" state with the givenreason.- The promise should start in the "pending" state.
- The constructor should accept an
-
State Management:
- The promise should have three possible states:
pending,fulfilled, andrejected. - Once a promise is fulfilled or rejected, its state and value/reason cannot be changed.
- The promise should have three possible states:
-
.then(onFulfilled, onRejected)Method:- This method registers callbacks to be executed when the promise is settled.
onFulfilled: A function to be called if the promise is fulfilled. It receives the fulfillmentvalueas an argument.onRejected: A function to be called if the promise is rejected. It receives the rejectionreasonas an argument.- If the promise is already settled when
.then()is called, the appropriate callback should be executed asynchronously (usingsetTimeoutor a similar mechanism to simulate microtask queuing). .then()itself should return a new promise. This returned promise should:- Be fulfilled with the return value of
onFulfilledoronRejectedif they return a non-promise value. - Be fulfilled or rejected based on the settlement of the promise returned by
onFulfilledoronRejected(if they return a promise). - Be rejected if
onFulfilledoronRejectedthrows an error.
- Be fulfilled with the return value of
-
.catch(onRejected)Method:- This is a shorthand for
.then(null, onRejected). It registers a callback to handle rejections. .catch()should also return a new promise.
- This is a shorthand for
-
Asynchronous Execution:
- All callbacks (
resolve,reject,onFulfilled,onRejected) should be executed asynchronously. This means they should not run immediately when called, but rather be queued for execution on the next tick of the event loop.setTimeout(callback, 0)is a common way to achieve this for this challenge.
- All callbacks (
Expected Behavior:
- Promises should correctly transition through states.
- Callbacks should be executed in the correct order and with the correct arguments.
- Chaining of promises via
.then()should work as expected. - Handling of rejections via
.catch()and theonRejectedargument of.then()should function properly. - Returning promises from
.then()handlers should correctly propagate their settlement.
Edge Cases to Consider:
- Calling
resolveorrejectmultiple times on the same promise. - Calling
.then()after the promise has already been settled. - Returning promises from
.then()handlers. - Throwing errors within
.then()handlers. - Passing non-function values to
.then()and.catch().
Examples
Example 1: Basic Fulfillment
const myPromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Success!');
}, 100);
});
myPromise.then(
(value) => {
console.log('Fulfilled:', value); // Expected: Fulfilled: Success!
},
(reason) => {
console.error('Rejected:', reason);
}
);
Explanation: The executor function calls resolve('Success!') after a 100ms delay. The onFulfilled callback in .then() is then executed asynchronously, logging "Fulfilled: Success!".
Example 2: Basic Rejection
const myPromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
reject('Failure!');
}, 100);
});
myPromise.then(
(value) => {
console.log('Fulfilled:', value);
},
(reason) => {
console.error('Rejected:', reason); // Expected: Rejected: Failure!
}
);
myPromise.catch((reason) => {
console.error('Caught in catch:', reason); // Expected: Caught in catch: Failure!
});
Explanation: The executor function calls reject('Failure!') after a 100ms delay. Both the onRejected callback in .then() and the .catch() handler are executed asynchronously, logging the rejection reason.
Example 3: Chaining and Promise Return
const myPromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(10);
}, 100);
});
myPromise
.then((value) => {
console.log('First then:', value); // Expected: First then: 10
return value * 2; // Returns a non-promise value
})
.then((value) => {
console.log('Second then:', value); // Expected: Second then: 20
return new MyPromise((resolve) => setTimeout(() => resolve(value + 5), 50)); // Returns a promise
})
.then((value) => {
console.log('Third then:', value); // Expected: Third then: 25
})
.catch((error) => {
console.error('Error:', error);
});
Explanation: The first .then() receives 10, returns 20. The second .then() receives 20, and returns a new promise that resolves to 25 after 50ms. The third .then() receives 25 and logs it. The chaining works because each .then() returns a new promise that resolves based on the previous step.
Example 4: Error Handling in then
const myPromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Initial Value');
}, 100);
});
myPromise
.then((value) => {
console.log('First then:', value); // Expected: First then: Initial Value
throw new Error('Something went wrong in then');
})
.then((value) => {
console.log('Second then:', value); // This won't be called
})
.catch((error) => {
console.error('Caught error:', error.message); // Expected: Caught error: Something went wrong in then
});
Explanation: The first .then() handler executes, logs the value, and then throws an error. This error causes the promise returned by the first .then() to be rejected. The .catch() handler then intercepts this rejection.
Constraints
- Your
MyPromiseclass should be written in plain JavaScript. - No external libraries or polyfills for Promises are allowed.
- You must use
setTimeout(callback, 0)(or a similar browser-native non-blocking mechanism) to ensure asynchronous execution of callbacks. - The implementation should aim for correctness and robustness, handling the specified behaviors and edge cases.
Notes
- Consider how to store the state, the value/reason, and the pending callbacks.
- The core logic will involve checking the promise's state when
.then()or.catch()is called, and whenresolve()orreject()are invoked. - The returned promise from
.then()needs to be carefully managed, linking its settlement to the outcome of the handlers or the returned promise from those handlers. - Remember that the
executorfunction is called immediately uponnew MyPromise(). - Pay close attention to the "thenable" concept when handling promises returned from
.then()callbacks. A "thenable" is an object with a.then()method. Your implementation should be able to handle both native Promises and instances ofMyPromisereturned from handlers.