Implementing a Custom Promise from Scratch
To implement a custom Promise, one must understand its fundamental behavior: it is a constructor that accepts a executor function (which receives resolve and reject callbacks). The state of a Promise transitions from pending to either fulfilled (resolved) or rejected, and the then method allows us to register callbacks to handle these outcomes.
Synchronous Promise Implementation
A basic implementation requires tracking the status and the result value. We use a helper variable to maintain the context (this) because standard function declarations inside the constructor create they own scope.
function CustomPromise(executor) {
this.state = 'pending';
this.value = undefined;
const self = this;
function resolve(result) {
if (self.state === 'pending') {
self.state = 'fulfilled';
self.value = result;
}
}
function reject(reason) {
if (self.state === 'pending') {
self.state = 'rejected';
self.value = reason;
}
}
executor(resolve, reject);
}
CustomPromise.prototype.then = function(onFulfilled, onRejected) {
if (this.state === 'fulfilled') {
onFulfilled(this.value);
} else if (this.state === 'rejected') {
onRejected(this.value);
}
};
Handling Asynchronous Operations
When the resolve or reject function is called asynchronously (e.g., inside a setTimeout), the then method will be executed while the state is still pending. To handle this, we must queue the callbacks in an array and trigger them once the state finally changes.
function AsyncPromise(executor) {
this.state = 'pending';
this.result = undefined;
this.observers = []; // Queue for async callbacks
const self = this;
function resolve(val) {
if (self.state === 'pending') {
self.state = 'fulfilled';
self.result = val;
self.observers.forEach(obs => obs.success(val));
}
}
function reject(err) {
if (self.state === 'pending') {
self.state = 'rejected';
self.result = err;
self.observers.forEach(obs => obs.failure(err));
}
}
executor(resolve, reject);
}
AsyncPromise.prototype.then = function(successCallback, failureCallback) {
if (this.state === 'fulfilled') {
successCallback(this.result);
} else if (this.state === 'rejected') {
failureCallback(this.result);
} else {
// Register callbacks if state is still pending
this.observers.push({
success: successCallback,
failure: failureCallback
});
}
};
Key Considerations
- Context (this): Using
const self = thisor capturing the scope correctly is vital. While arrow functions can preserve context, they make capturingargumentsobjects tricky, so standard function expressions are often safer for the internal resolve/reject handlers. - Execution Order: The
thenmethod executes synchronously. If the Promise is asynchronous, the registration of the success/failure handlers happens before the asynchronous operation completes, which is why the observer pattern (theobserversarray) is necessary. - State Locking: A Promise can only settle once. Adding checks (
if (this.state === 'pending')) ensures that subsequent calls toresolveorrejectdo not change the internal state or trigger handlers again.