Implement Custom Promise

A Promise is a fundamental concept in JavaScript for handling asynchronous operations. Understanding how to implement a custom Promise will deepen your understanding of asynchronous programming and help you in interviews.

Problem Statement

Implement a custom Promise class with the following methods:

  • constructor(executor) - Initialize the promise with an executor function
  • then(onFulfilled, onRejected) - Handle successful and failed states
  • catch(onRejected) - Handle only rejected states
  • finally(onFinally) - Execute regardless of state
  • static resolve(value) - Create a resolved promise
  • static reject(reason) - Create a rejected promise
  • static all(promises) - Wait for all promises to resolve
  • static race(promises) - Return the first settled promise

Solution

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());
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(callback => callback());
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    return new CustomPromise((resolve, reject) => {
      const handleFulfilled = () => {
        try {
          if (typeof onFulfilled === 'function') {
            const result = onFulfilled(this.value);
            resolve(result);
          } else {
            resolve(this.value);
          }
        } catch (error) {
          reject(error);
        }
      };

      const handleRejected = () => {
        try {
          if (typeof onRejected === 'function') {
            const result = onRejected(this.reason);
            resolve(result);
          } else {
            reject(this.reason);
          }
        } catch (error) {
          reject(error);
        }
      };

      if (this.state === 'fulfilled') {
        setTimeout(handleFulfilled, 0);
      } else if (this.state === 'rejected') {
        setTimeout(handleRejected, 0);
      } else {
        this.onFulfilledCallbacks.push(handleFulfilled);
        this.onRejectedCallbacks.push(handleRejected);
      }
    });
  }

  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((resolve, reject) => reject(reason));
  }

  static all(promises) {
    return new CustomPromise((resolve, reject) => {
      const results = [];
      let completedCount = 0;

      if (promises.length === 0) {
        resolve(results);
        return;
      }

      promises.forEach((promise, index) => {
        CustomPromise.resolve(promise).then(
          value => {
            results[index] = value;
            completedCount++;
            if (completedCount === promises.length) {
              resolve(results);
            }
          },
          reason => reject(reason)
        );
      });
    });
  }

  static race(promises) {
    return new CustomPromise((resolve, reject) => {
      promises.forEach(promise => {
        CustomPromise.resolve(promise).then(resolve, reject);
      });
    });
  }
}

Usage Example

// Basic usage
const promise = new CustomPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);
});

promise
  .then(value => {
    console.log(value); // "Success!"
    return value.toUpperCase();
  })
  .then(value => {
    console.log(value); // "SUCCESS!"
  })
  .catch(error => {
    console.error(error);
  })
  .finally(() => {
    console.log('Promise settled');
  });

// Static methods
CustomPromise.all([
  CustomPromise.resolve(1),
  CustomPromise.resolve(2),
  CustomPromise.resolve(3)
]).then(values => {
  console.log(values); // [1, 2, 3]
});

CustomPromise.race([
  new CustomPromise(resolve => setTimeout(() => resolve('First'), 100)),
  new CustomPromise(resolve => setTimeout(() => resolve('Second'), 200))
]).then(value => {
  console.log(value); // "First"
});

Key Concepts

  1. State Management: Promises have three states - pending, fulfilled, rejected
  2. Callback Queuing: Store callbacks when promise is pending, execute when settled
  3. Chaining: then returns a new promise for method chaining
  4. Error Handling: Proper error propagation through the chain
  5. Asynchronous Execution: Use setTimeout to ensure callbacks run asynchronously

Common Interview Questions

  • How does Promise chaining work internally?
  • What's the difference between Promise.all and Promise.allSettled?
  • How would you implement Promise.allSettled?
  • What happens if you don't handle promise rejections?
  • How do you convert callback-based APIs to promises?

Related Topics

  • Async/Await
  • Generator Functions
  • Event Loop
  • Error Handling Patterns
  • Promise Combinators