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
MyPromise
constructor takes a function calledexecutor
as an argument. This function is expected to take two parameters:resolve
andreject
. These are callbacks that theexecutor
will call based on the outcome of the asynchronous operation. - Inside the constructor, we initialize an empty
queue
to store the.then
callbacks. - We also set up placeholders for
errorHandler
(for.catch
) andfinallyHandler
(for.finally
), initializing them as no-op functions. - Finally, the
executor
function is invoked withonResolve
andonReject
bound tothis
to 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
onResolve
method is called with the resolveddata
. - This method iterates over the
queue
of.then
callbacks, passing the data through each one sequentially. This allows chaining of.then
calls, where the output of one.then
becomes the input for the next. - After all
.then
callbacks have been executed, thefinallyHandler
is 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,
onReject
is invoked with the error. - The
errorHandler
(set via.catch
) is called with the error. - Just like in
onResolve
, thefinallyHandler
is invoked to ensure any cleanup is performed.
- If the promise is rejected,
- Chaining with
.then
:then(callback) { this.queue.push(callback); return this; }
- The
.then
method allows you to add a callback to thequeue
. - After adding the callback,
this
is returned, enabling the chaining of multiple.then
calls.
- The
- Handling Errors with
.catch
:catch(callback) { this.errorHandler = callback; return this; }
- The
.catch
method sets theerrorHandler
to the provided callback function. - Similar to
.then
,this
is returned to support chaining.
- The
- Final Cleanup with
.finally
:finally(callback) { this.finallyHandler = callback; return this; }
- The
.finally
method sets thefinallyHandler
to 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
MyPromise
and pass anexecutor
function that simulates an asynchronous operation usingsetTimeout
. - After 1 second, the
resolve
function is called with the string"Data received!"
.
- We create an instance of
- Chaining
.then
Calls:- The first
.then
call logs the resolved data ("Data received!"
) and returns"Processed Data"
, which is passed to the next.then
in the chain. - The second
.then
call logs the processed data ("Processed Data"
).
- The first
- Handling Errors:
- If an error occurs during the promise’s execution, the
.catch
method will log the error. In our example, no error is generated, so.catch
is not triggered.
- If an error occurs during the promise’s execution, the
- Final Cleanup:
- The
.finally
method 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.