Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing the Promises/A+ Specification from Scratch in JavaScript

Tech 1

A Promise implementation begins as a class. The constructor receives a callback, commonly referred to as the executor, which runs immediately with two functional parameters: resolve and reject.

class MyPromise {
  constructor(executor) {
    const resolve = () => {};
    const reject = () => {};
    executor(resolve, reject);
  }
}

State Management and Core Rules

According to the specification, a promise must have three mutually exclusive states: pending, fulfilled, and rejected. When pending transitions to fulfilled, an immutable value is stored. On transition to rejected, an immutable reason is stored. Subsequent transitions are ignored. If the executor throws an error, reject is invoked automatically.

class MyPromise {
  constructor(executor) {
    this._state = 'pending';
    this._value = undefined;
    this._reason = undefined;

    const resolve = (value) => {
      if (this._state === 'pending') {
        this._state = 'fulfilled';
        this._value = value;
      }
    };

    const reject = (reason) => {
      if (this._state === 'pending') {
        this._state = 'rejected';
        this._reason = reason;
      }
    };

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

The then Method and Asynchronous Behavior

The then method registers onFulfilled and onRejected handlers. If the state is already settled, the matching handler is called synchronously. For pending promises, subscription lists store the callbacks so they can be invoked later, which enables asynchronous resolution.

class MyPromise {
  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((fn) => fn());
      }
    };

    const reject = (reason) => {
      if (this._state === 'pending') {
        this._state = 'rejected';
        this._reason = reason;
        this._onRejectedCallbacks.forEach((fn) => fn());
      }
    };

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

  then(onFulfilled, onRejected) {
    if (this._state === 'fulfilled') {
      onFulfilled(this._value);
    } else if (this._state === 'rejected') {
      onRejected(this._reason);
    } else if (this._state === 'pending') {
      this._onFulfilledCallbacks.push(() => onFulfilled(this._value));
      this._onRejectedCallbacks.push(() => onRejected(this._reason));
    }
  }
}

Enabling Chaining with promise2

To support chaining like promise.then(...).then(...), the then method defines and returns a new MyPromise instance called promise2. Whatever the handler returns becomes the input x for the Promise Resolution Procedure.

then(onFulfilled, onRejected) {
  const currentPromise = new MyPromise((resolve, reject) => {
    if (this._state === 'fulfilled') {
      const x = onFulfilled(this._value);
      resolvePromise(currentPromise, x, resolve, reject);
    } else if (this._state === 'rejected') {
      const x = onRejected(this._reason);
      resolvePromise(currentPromise, x, resolve, reject);
    } else if (this._state === 'pending') {
      this._onFulfilledCallbacks.push(() => {
        const x = onFulfilled(this._value);
        resolvePromise(currentPromise, x, resolve, reject);
      });
      this._onRejectedCallbacks.push(() => {
        const x = onRejected(this._reason);
        resolvePromise(currentPromise, x, resolve, reject);
      });
    }
  });
  return currentPromise;
}

The Promise Resolution Procedure

The recursive function resolvePromise handles the relationship between promise2 and x. Circular references cause a TypeError. If x is a thenable (an object or function with a then method), its then is called with resolve and reject callbacks, but only one settlement is allowed via a guard. Non-thenable values are passed to resolve directly.

function resolvePromise(targetPromise, x, resolve, reject) {
  if (targetPromise === x) {
    return reject(new TypeError('Chaining cycle detected'));
  }

  let settled = false;

  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      const then = x.then;
      if (typeof then === 'function') {
        then.call(
          x,
          (value) => {
            if (settled) return;
            settled = true;
            resolvePromise(targetPromise, value, resolve, reject);
          },
          (error) => {
            if (settled) return;
            settled = true;
            reject(error);
          }
        );
      } else {
        resolve(x);
      }
    } catch (error) {
      if (settled) return;
      settled = true;
      reject(error);
    }
  } else {
    resolve(x);
  }
}

Handling Optional Handlers and Asynchronicity

If onFulfilled or onRejected are not functions, defaults propagate the value or the error. All handler invocations happen asynchronously via setTimeout. Errors thrown inside handlers are caught and forwarded to reject.

then(onFulfilled, onRejected) {
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (error) => {
          throw error;
        };

  const wrapperPromise = new MyPromise((resolve, reject) => {
    const invokeFulfilled = () => {
      setTimeout(() => {
        try {
          const x = onFulfilled(this._value);
          resolvePromise(wrapperPromise, x, resolve, reject);
        } catch (error) {
          reject(error);
        }
      }, 0);
    };

    const invokeRejected = () => {
      setTimeout(() => {
        try {
          const x = onRejected(this._reason);
          resolvePromise(wrapperPromise, x, resolve, reject);
        } catch (error) {
          reject(error);
        }
      }, 0);
    };

    if (this._state === 'fulfilled') {
      invokeFulfilled();
    } else if (this._state === 'rejected') {
      invokeRejected();
    } else if (this._state === 'pending') {
      this._onFulfilledCallbacks.push(invokeFulfilled);
      this._onRejectedCallbacks.push(invokeRejected);
    }
  });

  return wrapperPromise;
}

Static Helpers: resolve, reject, race, and all

Common utility methods wrap values, aggregate multiple promises, or return the first settled result.

MyPromise.resolve = function (value) {
  return new MyPromise((resolve) => resolve(value));
};

MyPromise.reject = function (reason) {
  return new MyPromise((_, reject) => reject(reason));
};

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

MyPromise.all = function (promises) {
  return new MyPromise((resolve, reject) => {
    const results = [];
    let settledCount = 0;

    function collect(index, value) {
      results[index] = value;
      settledCount += 1;
      if (settledCount === promises.length) {
        resolve(results);
      }
    }

    promises.forEach((promise, index) => {
      promise.then((value) => collect(index, value), reject);
    });
  });
};

Running the Compliance Test Suite

To verify correctness, add the following deferred utility and export the class. Then install and execute promises-aplus-tests.

MyPromise.defer = MyPromise.deferred = function () {
  const deferred = {};
  deferred.promise = new MyPromise((resolve, reject) => {
    deferred.resolve = resolve;
    deferred.reject = reject;
  });
  return deferred;
};

module.exports = MyPromise;

Run the tests from the terminal:

npm install -g promises-aplus-tests
promises-aplus-tests your-file.js

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.