Implementing Custom JavaScript Promises



Creating a custom implementation of JavaScript Promises from scratch is an excellent exercise to understand how Promises work under the hood. This implementation will include basic functionality for resolving, rejecting, chaining with then, handling errors with catch, and ensuring the finally method is available.


What is a Promise?

A Promise is an object representing the eventual completion or failure of an asynchronous operation. Promises provide a cleaner way to handle asynchronous operations compared to callbacks, offering methods like then, catch, and finally for chaining and error handling.

Real Interview Insights

Interviewers might ask you to:

  • Implement a custom Promise class from scratch.
  • Explain the internal mechanics of Promises.
  • Demonstrate the usage of your custom Promise class with practical examples.

Implementing Custom JavaScript Promises

Here’s a basic implementation of a custom Promise:

class CustomPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
 
    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(callback => callback(value));
      }
    };
 
    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(callback => callback(reason));
      }
    };
 
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
 
  then(onFulfilled, onRejected) {
    return new CustomPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        try {
          const result = onFulfilled(this.value);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      } else if (this.state === 'rejected') {
        try {
          const result = onRejected(this.reason);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      } else {
        this.onFulfilledCallbacks.push((value) => {
          try {
            const result = onFulfilled(value);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        });
 
        this.onRejectedCallbacks.push((reason) => {
          try {
            const result = onRejected(reason);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        });
      }
    });
  }
 
  catch(onRejected) {
    return this.then(null, onRejected);
  }
 
  finally(onFinally) {
    return this.then(
      value => {
        onFinally();
        return value;
      },
      reason => {
        onFinally();
        throw reason;
      }
    );
  }
 
  static resolve(value) {
    return new CustomPromise((resolve) => resolve(value));
  }
 
  static reject(reason) {
    return new CustomPromise((_, reject) => reject(reason));
  }
 
  static all(promises) {
    return new CustomPromise((resolve, reject) => {
      let results = [];
      let completedPromises = 0;
 
      promises.forEach((promise, index) => {
        CustomPromise.resolve(promise)
          .then(value => {
            results[index] = value;
            completedPromises++;
            if (completedPromises === promises.length) {
              resolve(results);
            }
          })
          .catch(reject);
      });
    });
  }
 
  static race(promises) {
    return new CustomPromise((resolve, reject) => {
      promises.forEach(promise => {
        CustomPromise.resolve(promise)
          .then(resolve)
          .catch(reject);
      });
    });
  }
 
  static allSettled(promises) {
    return new CustomPromise((resolve) => {
      let results = [];
      let completedPromises = 0;
 
      promises.forEach((promise, index) => {
        CustomPromise.resolve(promise)
          .then(value => {
            results[index] = { status: 'fulfilled', value };
          })
          .catch(reason => {
            results[index] = { status: 'rejected', reason };
          })
          .finally(() => {
            completedPromises++;
            if (completedPromises === promises.length) {
              resolve(results);
            }
          });
      });
    });
  }
 
  static any(promises) {
    return new CustomPromise((resolve, reject) => {
      let errors = [];
      let rejectedPromises = 0;
 
      promises.forEach((promise, index) => {
        CustomPromise.resolve(promise)
          .then(resolve)
          .catch(error => {
            errors[index] = error;
            rejectedPromises++;
            if (rejectedPromises === promises.length) {
              reject(new AggregateError(errors, 'All promises were rejected'));
            }
          });
      });
    });
  }
}

Explanation:

  • Constructor: Initializes the promise with a state (pending, fulfilled, rejected), value, reason, and arrays for success and failure callbacks.
  • resolve and reject Functions: Change the state and call the respective callbacks.
  • then Method: Chains promises by returning a new CustomPromise. Handles the value or reason based on the state.
  • catch Method: Simplifies attaching rejection handlers.
  • finally Method: Ensures a callback runs regardless of the promise's outcome.
  • Static Methods: Implement common Promise methods (resolve, reject, all, race, allSettled, any).

Practical Example

const promise1 = new CustomPromise((resolve) => setTimeout(resolve, 100, 'First'));
const promise2 = new CustomPromise((resolve, reject) => setTimeout(reject, 200, 'Second'));
const promise3 = new CustomPromise((resolve) => setTimeout(resolve, 300, 'Third'));
 
promise1
  .then(result => {
    console.log(result);
    return 'Next';
  })
  .then(result => console.log(result)) // Output: 'First', 'Next'
  .catch(error => console.error(error))
  .finally(() => console.log('Cleanup'));
 
CustomPromise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error)); // Output: 'Second'