In JavaScript, Promises are a powerful way to handle asynchronous operations. They provide a mechanism to execute code after an asynchronous task completes, with support for chaining multiple tasks using the .then method. In this blog post, we’ll explore how to implement a basic version of a Promise from scratch, including support for .then, .catch, and .finally methods.
Let’s dive into the code and break down each part of our custom Promise implementation, called MyPromise.
The MyPromise Class
Here’s the complete code for our custom MyPromise implementation:
class MyPromise {
constructor(executor) {
this.queue = [];
this.errorHandler = () => {};
this.finallyHandler = () => {};
executor(this.onResolve.bind(this), this.onReject.bind(this));
}
onResolve(data) {
this.queue.forEach(callback => {
data = callback(data);
});
this.finallyHandler();
}
onReject(error) {
this.errorHandler(error);
this.finallyHandler();
}
then(callback) {
this.queue.push(callback);
return this;
}
catch(callback) {
this.errorHandler = callback;
return this;
}
finally(callback) {
this.finallyHandler = callback;
return this;
}
}
Breaking Down the Code
- Constructor and Initial Setup:
constructor(executor) { this.queue = []; this.errorHandler = () => {}; this.finallyHandler = () => {}; executor(this.onResolve.bind(this), this.onReject.bind(this)); }- The
MyPromiseconstructor takes a function calledexecutoras an argument. This function is expected to take two parameters:resolveandreject. These are callbacks that theexecutorwill call based on the outcome of the asynchronous operation. - Inside the constructor, we initialize an empty
queueto store the.thencallbacks. - We also set up placeholders for
errorHandler(for.catch) andfinallyHandler(for.finally), initializing them as no-op functions. - Finally, the
executorfunction is invoked withonResolveandonRejectbound tothisto ensure they have access to the instance’s properties.
- The
- Handling the Resolution of the Promise:
onResolve(data) { this.queue.forEach(callback => { data = callback(data); }); this.finallyHandler(); }- When the promise is resolved, the
onResolvemethod is called with the resolveddata. - This method iterates over the
queueof.thencallbacks, passing the data through each one sequentially. This allows chaining of.thencalls, where the output of one.thenbecomes the input for the next. - After all
.thencallbacks have been executed, thefinallyHandleris called, ensuring any cleanup code runs regardless of the promise’s outcome.
- When the promise is resolved, the
- Handling Rejection:
onReject(error) { this.errorHandler(error); this.finallyHandler(); }- If the promise is rejected,
onRejectis invoked with the error. - The
errorHandler(set via.catch) is called with the error. - Just like in
onResolve, thefinallyHandleris invoked to ensure any cleanup is performed.
- If the promise is rejected,
- Chaining with
.then:then(callback) { this.queue.push(callback); return this; }- The
.thenmethod allows you to add a callback to thequeue. - After adding the callback,
thisis returned, enabling the chaining of multiple.thencalls.
- The
- Handling Errors with
.catch:catch(callback) { this.errorHandler = callback; return this; }- The
.catchmethod sets theerrorHandlerto the provided callback function. - Similar to
.then,thisis returned to support chaining.
- The
- Final Cleanup with
.finally:finally(callback) { this.finallyHandler = callback; return this; }- The
.finallymethod sets thefinallyHandlerto the provided callback function. - This method also supports chaining by returning
this.
- The
Testing the MyPromise Implementation
Now, let’s test our MyPromise implementation with an example:
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve("Data received!");
}, 1000);
});
promise
.then(data => {
console.log(data); // "Data received!"
return "Processed Data";
})
.then(data => {
console.log(data); // "Processed Data"
})
.catch(error => {
console.log("Error:", error);
})
.finally(() => {
console.log("Promise completed.");
});
Explanation:
- Creating a New Promise:
- We create an instance of
MyPromiseand pass anexecutorfunction that simulates an asynchronous operation usingsetTimeout. - After 1 second, the
resolvefunction is called with the string"Data received!".
- We create an instance of
- Chaining
.thenCalls:- The first
.thencall logs the resolved data ("Data received!") and returns"Processed Data", which is passed to the next.thenin the chain. - The second
.thencall logs the processed data ("Processed Data").
- The first
- Handling Errors:
- If an error occurs during the promise’s execution, the
.catchmethod will log the error. In our example, no error is generated, so.catchis not triggered.
- If an error occurs during the promise’s execution, the
- Final Cleanup:
- The
.finallymethod logs"Promise completed.", ensuring that this message is printed regardless of whether the promise was resolved or rejected.
- The
Conclusion
By building this simple implementation of a Promise, we gain a deeper understanding of how Promises work under the hood in JavaScript. The custom MyPromise class demonstrates the core concepts of Promise resolution, rejection, and method chaining using .then, .catch, and .finally. While this implementation covers the basics, real-world JavaScript Promises offer more advanced features and error handling capabilities. Nonetheless, creating your own Promise implementation is an excellent exercise to solidify your understanding of asynchronous programming in JavaScript.
