Introduction 🌟
JavaScript’s Promise
is a powerful tool for managing asynchronous operations, but what if we wanted to build our own version of a promise to understand how it works under the hood? In this post, we’ll walk through the process of creating a basic custom promise from scratch and explore various modifications to enhance its functionality. By the end of this post, we have a deep understanding of promises, how they work, and how to extend them.
How to Approach Building a Custom Promise 🔧
Before we dive into the code, let’s break down the core concepts we need to understand when creating a promise. A promise is essentially a placeholder for a value that may be available now or in the future, usually after an asynchronous operation completes.
Core Components of a Promise 🛠️:
-
State Management: A promise can be in one of three states:
- Pending ⏳: The promise is still executing.
- Fulfilled ✅: The promise has successfully completed.
- Rejected ❌: The promise has failed.
Callbacks: Promises allow us to attach success (
then
) and failure (catch
) callbacks, which will be executed once the promise is either fulfilled or rejected.
Step-by-Step: Building the Basic Custom Promise 🏗️
We’ll start by creating a custom promise that can resolve or reject based on an asynchronous operation. Here’s how we approach it:
-
Define the States: We'll manage the states (
pending
,fulfilled
, andrejected
) within our custom promise. - Store Callbacks: We’ll store the success and error handlers in arrays, so they can be called later once the promise settles.
- Resolve and Reject Logic: We’ll implement methods to change the promise’s state and invoke the appropriate callbacks.
Here's a simple implementation:
// Custom Promise constructor
class CustomPromise {
constructor(executor) {
this.state = 'pending'; // Possible states: 'pending', 'fulfilled', 'rejected'
this.value = undefined; // Will hold the result or error
this.successCallbacks = [];
this.errorCallbacks = [];
// Executor is the function passed to the promise
try {
console.log("Executing the promise... 🎯");
executor(this._resolve, this._reject);
} catch (error) {
this._reject(error);
}
}
// Custom resolve function
_resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
console.log(`Promise resolved with: ${value} ✅`);
this.successCallbacks.forEach(callback => callback(this.value));
}
}
// Custom reject function
_reject = (error) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.value = error;
console.log(`Promise rejected with: ${error} ❌`);
this.errorCallbacks.forEach(callback => callback(this.value));
}
}
// Then method to handle successful promise resolution
then(successCallback) {
if (this.state === 'fulfilled') {
successCallback(this.value);
} else if (this.state === 'pending') {
this.successCallbacks.push(successCallback);
}
return this; // Allows chaining 🔄
}
// Catch method to handle promise rejection
catch(errorCallback) {
if (this.state === 'rejected') {
errorCallback(this.value);
} else if (this.state === 'pending') {
this.errorCallbacks.push(errorCallback);
}
return this; // Allows chaining 🔄
}
}
Explanation 📚:
-
State and Value: We initialize the state to
pending
and the value toundefined
. The value will hold the result or the error, depending on whether the promise is fulfilled or rejected. -
Callbacks: We maintain two arrays,
successCallbacks
anderrorCallbacks
, where we store the callbacks that should be called once the promise is resolved or rejected. -
Executor: The
executor
function passed to the promise constructor is responsible for triggering the resolution or rejection of the promise. -
Resolution: The
_resolve
method changes the state tofulfilled
and calls all success callbacks. -
Rejection: The
_reject
method changes the state torejected
and calls all error callbacks.
Using the Custom Promise 🖥️:
Now, let's use the custom promise and log the results so we can see the promise in action:
const myCustomPromise = new CustomPromise((resolve, reject) => {
setTimeout(() => {
const success = true; // Change this to false to test rejection
if (success) {
resolve("Custom promise resolved successfully! 🎉");
} else {
reject("Custom promise was rejected! 😔");
}
}, 2000); // 2-second delay ⏳
});
myCustomPromise
.then((message) => {
console.log("Inside then:", message); // Will print if promise is resolved
})
.catch((error) => {
console.error("Inside catch:", error); // Will print if promise is rejected
});
Expected Output:
If the success
variable is set to true
, the output will be:
Executing the promise... 🎯
Promise resolved with: Custom promise resolved successfully! 🎉
Inside then: Custom promise resolved successfully! 🎉
If the success
variable is set to false
, the output will be:
Executing the promise... 🎯
Promise rejected with: Custom promise was rejected! 😔
Inside catch: Custom promise was rejected! 😔
Enhancements and Modifications 🔧
Once we have the basic structure of the custom promise, we can improve it in several ways. Let’s explore some useful modifications:
1. Support for Chaining with Multiple then
and catch
Calls 🔄
Promises are often chained to handle successive asynchronous operations. We can enhance the then
method to return a new promise, allowing chaining of multiple handlers.
then(successCallback) {
const newPromise = new CustomPromise((resolve, reject) => {
const handleSuccess = (value) => {
try {
const result = successCallback(value);
if (result instanceof CustomPromise) {
result.then(resolve).catch(reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
};
if (this.state === 'fulfilled') {
handleSuccess(this.value);
} else if (this.state === 'pending') {
this.successCallbacks.push(handleSuccess);
}
});
return newPromise; // Return the new promise for chaining 🔄
}
2. Adding a finally
Method 🧹
A finally
method executes a callback regardless of whether the promise is resolved or rejected, typically used for cleanup tasks.
finally(callback) {
return this.then(
(value) => {
callback();
return value; // Pass the resolved value through
},
(error) => {
callback();
throw error; // Pass the rejection through
}
);
}
3. Timeout Functionality ⏰
Sometimes, we may want to enforce a timeout on a promise. If the promise doesn’t resolve in a given time, it automatically rejects.
timeout(ms) {
return new CustomPromise((resolve, reject) => {
const timer = setTimeout(() => {
reject('Promise timed out ⏳');
}, ms);
this.then((value) => {
clearTimeout(timer);
resolve(value);
}).catch((error) => {
clearTimeout(timer);
reject(error);
});
});
}
4. Static resolve
and reject
Methods ✅❌
To create promises that are already resolved or rejected, we can add static methods to mimic the behavior of JavaScript’s native Promise.resolve()
and Promise.reject()
.
static resolve(value) {
return new CustomPromise((resolve) => resolve(value));
}
static reject(error) {
return new CustomPromise((_, reject) => reject(error));
}
5. Adding all
and race
Methods ⚡
To work with multiple promises at once, we can implement all
(resolves when all promises are fulfilled) and race
(resolves as soon as the first promise settles).
static all(promises) {
return new CustomPromise((resolve, reject) => {
const results = [];
let completed = 0;
promises.forEach((promise, index) => {
promise.then((value) => {
results[index] = value;
completed++;
if (completed === promises.length) {
resolve(results);
}
}).catch(reject);
});
});
}
Conclusion 🎓
Creating a custom promise is an excellent way to understand how JavaScript handles asynchronous operations under the hood. In this post, we walked through the basic structure of a promise, added detailed logs to help us track its progress, and explored several modifications that we can implement to make it more powerful. From supporting chaining and finally
methods to adding timeout functionality and handling multiple promises, these modifications bring our custom promise closer to JavaScript’s native Promise
.
As we continue to experiment with asynchronous programming, building our own promise-like implementation will give us a solid foundation for understanding the inner workings of JavaScript’s concurrency model. Happy coding! 🎉
Top comments (0)