DEV Community

Cover image for A Must Know Js Question: Creating a Basic Custom Promise 🌟
Abhinav
Abhinav

Posted on

A Must Know Js Question: Creating a Basic Custom Promise 🌟

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 🛠️:

  1. 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.
  2. 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:

  1. Define the States: We'll manage the states (pending, fulfilled, and rejected) within our custom promise.
  2. Store Callbacks: We’ll store the success and error handlers in arrays, so they can be called later once the promise settles.
  3. 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 🔄
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation 📚:

  • State and Value: We initialize the state to pending and the value to undefined. The value will hold the result or the error, depending on whether the promise is fulfilled or rejected.
  • Callbacks: We maintain two arrays, successCallbacks and errorCallbacks, 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 to fulfilled and calls all success callbacks.
  • Rejection: The _reject method changes the state to rejected 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
    });
Enter fullscreen mode Exit fullscreen mode

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! 🎉
Enter fullscreen mode Exit fullscreen mode

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! 😔
Enter fullscreen mode Exit fullscreen mode

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 🔄
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    );
}
Enter fullscreen mode Exit fullscreen mode

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);
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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);
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

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)