Writing Your Own Promise

Inner Workings: Writing Your Own Promise

A promise is a small state machine: a state, a value and a list of reactions. Settling stores the value and queues each reaction as a microtask. then() creates the next promise plus a reaction that feeds the handler's result into that promise's resolve, which is all chaining is. Resolving with a thenable (any object with a then method) subscribes to it instead of storing it.

A minimal promise with chaining, thenable adoption and error propagationJavaScriptLive
class MyPromise {
  #state = "pending"; #value; #reactions = [];
  constructor(executor) {
    let called = false;                          // resolve and reject work only once
    const once = fn => arg => { if (!called) { called = true; fn(arg); } };
    const reject = once(e => this.#settle("rejected", e));
    try { executor(once(v => this.#adopt(v)), reject); } catch (err) { reject(err); }
  }
  #adopt(value) {                                // follow a thenable, or fulfill with the value
    if (typeof value?.then !== "function") return this.#settle("fulfilled", value);
    queueMicrotask(() => value.then(v => this.#adopt(v), e => this.#settle("rejected", e)));
  }
  #settle(state, value) {
    if (this.#state !== "pending") return;
    [this.#state, this.#value] = [state, value];
    this.#reactions.forEach(queueMicrotask);     // run waiting reactions as microtasks
  }
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const run = () => {
        const ok = this.#state === "fulfilled", handler = ok ? onFulfilled : onRejected;
        if (typeof handler !== "function") return (ok ? resolve : reject)(this.#value);
        try { resolve(handler(this.#value)); } catch (err) { reject(err); }
      };
      this.#state === "pending" ? this.#reactions.push(run) : queueMicrotask(run);
    });
  }
  catch(onRejected) { return this.then(undefined, onRejected); }
}
new MyPromise(resolve => setTimeout(resolve, 10, 21))
  .then(n => new MyPromise(resolve => resolve(n * 2)))
  .then(n => { throw new Error(`boom ${n}`); })
  .catch(err => console.log("caught:", err.message));

The native version also rejects a promise resolved with itself, catches a throwing then, and tracks unhandled rejections. The Promises/A+ test suite (https://github.com/promises-aplus/promises-tests 965 ) (npm 2,036 install promises-aplus-tests) checks an implementation against the rules.