Q31: How would you implement a simplified Promise from scratch?

Difficulty:AdvancedTopic:Asynchronous JavaScript

Answer

A promise represents the eventual fulfillment or rejection of an asynchronous operation. A simplified implementation needs to track its state, store the result, and queue handlers until the promise settles.

The implementation below demonstrates the core concepts. It is intentionally not a fully compliant ECMAScript Promise implementation.

class MyPromise {
  constructor(executor) {
    this.state = "pending";
    this.value = undefined;
    this.handlers = [];

    const resolve = value => {
      if (this.state !== "pending") return;

      if (value === this) {
        reject(new TypeError("A promise cannot resolve to itself"));
        return;
      }

      if (value instanceof MyPromise) {
        value.then(resolve, reject);
        return;
      }

      this.state = "fulfilled";
      this.value = value;
      this.flush();
    };

    const reject = reason => {
      if (this.state !== "pending") return;

      this.state = "rejected";
      this.value = reason;
      this.flush();
    };

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

  flush() {
    queueMicrotask(() => {
      const handlers = this.handlers.splice(0);

      for (const handler of handlers) {
        this.runHandler(handler);
      }
    });
  }

  runHandler(handler) {
    const callback =
      this.state === "fulfilled"
        ? handler.onFulfilled
        : handler.onRejected;

    if (typeof callback !== "function") {
      if (this.state === "fulfilled") {
        handler.resolve(this.value);
      } else {
        handler.reject(this.value);
      }

      return;
    }

    try {
      handler.resolve(callback(this.value));
    } catch (error) {
      handler.reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const handler = {
        onFulfilled,
        onRejected,
        resolve,
        reject
      };

      if (this.state === "pending") {
        this.handlers.push(handler);
      } else {
        queueMicrotask(() => this.runHandler(handler));
      }
    });
  }

  catch(onRejected) {
    return this.then(undefined, onRejected);
  }
}

new MyPromise(resolve => {
  setTimeout(() => resolve(10), 100);
}).then(value => value * 2)
  .then(console.log);
// 20

Explanation

The promise begins in the pending state and may transition only once to fulfilled or rejected. Handlers registered with then() run asynchronously through the microtask queue.

Important limitations

This implementation handles basic chaining and adopts other MyPromise instances, but it does not implement the complete thenable resolution procedure, static methods such as all() and race(), species constructors, unhandled-rejection tracking, or every specification edge case. A true promise polyfill is substantially more complex.

Key Takeaways
  • A promise has pending, fulfilled, and rejected states, with settlement occurring at most once.
  • Correct promise chaining requires asynchronous handler execution, error propagation, and thenable adoption.