Creating a Simple Implementation of a Promise with .then Chaining

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

  1. 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 called executor as an argument. This function is expected to take two parameters: resolve and reject. These are callbacks that the executor 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) and finallyHandler (for .finally), initializing them as no-op functions.
    • Finally, the executor function is invoked with onResolve and onReject bound to this to ensure they have access to the instance’s properties.
  2. 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 resolved data.
    • 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, the finallyHandler is called, ensuring any cleanup code runs regardless of the promise’s outcome.
  3. 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, the finallyHandler is invoked to ensure any cleanup is performed.
  4. Chaining with .then:then(callback) { this.queue.push(callback); return this; }
    • The .then method allows you to add a callback to the queue.
    • After adding the callback, this is returned, enabling the chaining of multiple .then calls.
  5. Handling Errors with .catch:catch(callback) { this.errorHandler = callback; return this; }
    • The .catch method sets the errorHandler to the provided callback function.
    • Similar to .then, this is returned to support chaining.
  6. Final Cleanup with .finally:finally(callback) { this.finallyHandler = callback; return this; }
    • The .finally method sets the finallyHandler to the provided callback function.
    • This method also supports chaining by returning this.

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 an executor function that simulates an asynchronous operation using setTimeout.
    • After 1 second, the resolve function is called with the string "Data received!".
  • 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").
  • 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.
  • Final Cleanup:
    • The .finally method logs "Promise completed.", ensuring that this message is printed regardless of whether the promise was resolved or rejected.

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.