FossilsπŸ’» CodingImplement Promise from Scratch
πŸ‘‘ApexJavaScriptPromisesPolyfill

Implement Promise from Scratch

Building a Promise implementation from scratch is the ultimate test of understanding async JavaScript. It covers state machines, microtask scheduling, chaining, and error propagation.

Implement Promise from Scratch

Interview Question: "Implement a basic Promise that supports then, catch, and chaining."

Minimal Implementation

class MyPromise {
  #state = 'pending';
  #value = undefined;
  #handlers = [];
 
  constructor(executor) {
    const resolve = (value) => {
      if (this.#state !== 'pending') return;
      if (value instanceof MyPromise) {
        value.then(resolve, reject);
        return;
      }
      this.#state = 'fulfilled';
      this.#value = value;
      this.#runHandlers();
    };
 
    const reject = (reason) => {
      if (this.#state !== 'pending') return;
      this.#state = 'rejected';
      this.#value = reason;
      this.#runHandlers();
    };
 
    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
 
  #runHandlers() {
    queueMicrotask(() => {
      this.#handlers.forEach(({ onFulfilled, onRejected, resolve, reject }) => {
        const handler = this.#state === 'fulfilled' ? onFulfilled : onRejected;
 
        if (!handler) {
          (this.#state === 'fulfilled' ? resolve : reject)(this.#value);
          return;
        }
 
        try {
          const result = handler(this.#value);
          if (result instanceof MyPromise) {
            result.then(resolve, reject);
          } else {
            resolve(result);
          }
        } catch (err) {
          reject(err);
        }
      });
      this.#handlers = [];
    });
  }
 
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this.#handlers.push({
        onFulfilled: typeof onFulfilled === 'function' ? onFulfilled : null,
        onRejected: typeof onRejected === 'function' ? onRejected : null,
        resolve,
        reject,
      });
 
      if (this.#state !== 'pending') {
        this.#runHandlers();
      }
    });
  }
 
  catch(onRejected) {
    return this.then(null, onRejected);
  }
 
  finally(onFinally) {
    return this.then(
      (value) => MyPromise.resolve(onFinally()).then(() => value),
      (reason) => MyPromise.resolve(onFinally()).then(() => { throw reason; })
    );
  }
 
  static resolve(value) {
    if (value instanceof MyPromise) return value;
    return new MyPromise((resolve) => resolve(value));
  }
 
  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
 
  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = [];
      let remaining = 0;
 
      for (const promise of promises) {
        const index = remaining;
        remaining++;
 
        MyPromise.resolve(promise).then(
          (value) => {
            results[index] = value;
            if (--remaining === 0) resolve(results);
          },
          reject
        );
      }
 
      if (remaining === 0) resolve([]);
    });
  }
}

Key Implementation Details

1. State Machine

A Promise is a state machine with three states and two transitions:

pending ──resolve──→ fulfilled
pending ──reject───→ rejected

Once settled, the state is immutable. This is why the guard if (this.#state !== 'pending') return exists.

2. Microtask Scheduling

queueMicrotask() ensures handlers run asynchronously, after the current synchronous code completes but before the next macrotask:

const p = new MyPromise(resolve => resolve(42));
p.then(v => console.log(v));
console.log('sync');
// Output: 'sync', 42 β€” handler runs after synchronous code

Without queueMicrotask, synchronously resolved promises would call handlers synchronously, breaking the guarantee that .then callbacks always run asynchronously.

3. Chaining Mechanics

Each .then() returns a new promise. The resolution of that new promise depends on what the handler returns:

promise
  .then(v => v + 1)        // Return value β†’ next promise resolves with it
  .then(v => { throw 'err'; }) // Throw β†’ next promise rejects
  .then(v => new MyPromise(r => r(v)))  // Return promise β†’ adopt its state
  .catch(err => 'recovered') // Catch returns β†’ chain continues as fulfilled

4. Value Propagation (Transparent Then)

If .then() is called without a handler for the current state, the value passes through:

MyPromise.reject('error')
  .then(v => v)      // No onRejected β€” error passes through
  .then(v => v)      // Still passes through
  .catch(e => e);    // Finally caught: 'error'

This is why the if (!handler) branch forwards to resolve/reject directly.

5. Promise Resolution with Thenable

A spec-compliant implementation also handles "thenables" (objects with a .then method):

const resolve = (value) => {
  if (value && typeof value.then === 'function') {
    value.then(resolve, reject);
    return;
  }
  // ... normal resolution
};

Test Cases

// Basic resolution
new MyPromise(r => r(42)).then(v => console.log(v)); // 42
 
// Chaining
MyPromise.resolve(1)
  .then(v => v + 1)
  .then(v => v * 2)
  .then(v => console.log(v)); // 4
 
// Error propagation
MyPromise.reject('fail')
  .then(v => v)
  .catch(e => console.log(e)); // 'fail'
 
// Async resolution
new MyPromise(r => setTimeout(() => r('delayed'), 100))
  .then(v => console.log(v)); // 'delayed' (after 100ms)
 
// Promise.all
MyPromise.all([
  MyPromise.resolve(1),
  MyPromise.resolve(2),
  MyPromise.resolve(3),
]).then(v => console.log(v)); // [1, 2, 3]
 
// finally
MyPromise.resolve('value')
  .finally(() => console.log('cleanup'))
  .then(v => console.log(v));
// 'cleanup', 'value'

Common Mistakes

  • Not using queueMicrotask β€” handlers must always be async even for already-resolved promises
  • Not handling the case where resolve is called with another Promise (recursive resolution)
  • Not guarding against double resolution (resolve called twice)
  • Forgetting that catch is just then(null, onRejected)
  • finally callback's return value is ignored (original value passes through) unless it throws or returns a rejected promise