Basic Javascript promise implementation attempt

There are a number of cases you’re not handling here. The best bet is to start by building the promise as a state machine:

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise() {

  // store state which can be PENDING, FULFILLED or REJECTED
  var state = PENDING;

  // store value once FULFILLED or REJECTED
  var value = null;

  // store sucess & failure handlers
  var handlers = [];
}

Now lets define a simple helper to use through the rest of our implementation:

// a function that returns `then` if `value` is a promise, otherwise `null`
function getThen(value) {
  if (value && (typeof value === 'object' || typeof value === 'function')) {
    var then = value.then;
    if (typeof then === 'function') {
      return then;
    }
  }
  return null;
}

Next, we need to consider each of the transformations that can occur:

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise() {

  // store state which can be PENDING, FULFILLED or REJECTED
  var state = PENDING;

  // store value once FULFILLED or REJECTED
  var value = null;

  // store sucess & failure handlers
  var handlers = [];

  function resolve(result) {
    try {
      var then = getThen(result);
      if (then) {
        doResolve(then.bind(result), resolve, reject)
        return
      }
      state = FULFILLED;
      value = result;
    } catch (e) {
      reject(e);
    }
  }

  function reject(error) {
    state = REJECTED;
    value = error;
  }
}

Note how resolve can receive a Promise as its argument, but a Promise can never be fulfilled with another Promise. So we have to handle this special case.

Note also that a Promise can only ever be fulfilled/rejected once. We also have the problem that a third party Promise may misbehave, and we should guard our code against that. For this reason, I haven’t just called result.then(resolve, reject) from within resolve. Instead, I split that into a separate function:

/**
 * Take a potentially misbehaving resolver function and make sure
 * onFulfilled and onRejected are only called once.
 *
 * Makes no guarantees about asynchrony.
 */
function doResolve(fn, onFulfilled, onRejected) {
  var done = false;
  try {
    fn(function (value) {
      if (done) return
      done = true
      onFulfilled(value)
    }, function (reason) {
      if (done) return
      done = true
      onRejected(reason)
    })
  } catch (ex) {
    if (done) return
    done = true
    onRejected(ex)
  }
}

So now we have a completed state machine, but no way to observe or trigger the changes in state. Lets start by adding a way to trigger the state changes by passing in a resolver function.

function Promise(fn) {
  if (typeof this !== 'object')
    throw new TypeError('Promises must be constructed via new');
  if (typeof fn !== 'function')
    throw new TypeError('fn must be a function');

  // store state which can be PENDING, FULFILLED or REJECTED
  var state = PENDING;

  // store value once FULFILLED or REJECTED
  var value = null;

  // store sucess & failure handlers
  var handlers = [];

  function resolve(result) {
    try {
      var then = getThen(result);
      if (then) {
        doResolve(then.bind(result), resolve, reject)
        return
      }
      state = FULFILLED;
      value = result;
    } catch (e) {
      reject(e);
    }
  }

  function reject(error) {
    state = REJECTED;
    value = error;
  }

  doResolve(fn, resolve, reject);
}

As you can see, we re-use doResolve because we have another un-trusted resolver. The fn might call resolve or reject multiple times, and it might throw an error. We need to handle all of these cases (and that’s what doResolve does).

We now have the completed state machine, but we haven’t exposed any information about what state it is in. Lets try adding a .done(onFulfilled, onRejected) method that is just like .then except that it does not return a Promise and does not handle errors thrown by onFulfilled and onRejected.

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise(fn) {
  if (typeof this !== 'object')
    throw new TypeError('Promises must be constructed via new');
  if (typeof fn !== 'function')
    throw new TypeError('fn must be a function');

  // store state which can be PENDING, FULFILLED or REJECTED
  var state = PENDING;

  // store value once FULFILLED or REJECTED
  var value = null;

  // store sucess & failure handlers
  var handlers = [];

  function resolve(result) {
    try {
      var then = getThen(result);
      if (then) {
        doResolve(then.bind(result), resolve, reject)
        return
      }
      state = FULFILLED;
      value = result;
      handlers.forEach(handle);
      handlers = null;
    } catch (e) {
      reject(e);
    }
  }

  function reject(error) {
    state = REJECTED;
    value = error;
    handlers.forEach(handle);
    handlers = null;
  }

  function handle(handler) {
    if (state === PENDING) {
      handlers.push(handler);
    } else {
      if (state === FULFILLED && typeof handler.onFulfilled === 'function') {
        handler.onFulfilled(value);
      }
      if (state === REJECTED && typeof handler.onRejected === 'function') {
        handler.onRejected(value);
      }
    }
  }
  this.done = function (onFulfilled, onRejected) {
    setTimeout(function () { // ensure we are always asynchronous
      handle({
        onFulfilled: onFulfilled,
        onRejected: onRejected
      });
    }, 0);
  }

  doResolve(fn, resolve, reject);
}

Note how we must handle the case of .done being called both before and after the Promise becomes fulfilled/rejected.

We almost have a complete promise implementation, but, as you already noticed when building your own implementation, we need a .then method that returns a Promise.

We can build this easilly out of .done:

this.then = function (onFulfilled, onRejected) {
  var self = this;
  return new Promise(function (resolve, reject) {
    return self.done(function (result) {
      if (typeof onFulfilled === 'function') {
        try {
          return resolve(onFulfilled(result));
        } catch (ex) {
          return reject(ex);
        }
      } else {
        return resolve(result);
      }
    }, function (error) {
      if (typeof onRejected === 'function') {
        try {
          return resolve(onRejected(error));
        } catch (ex) {
          return reject(ex);
        }
      } else {
        return reject(error);
      }
    });
  });
}

Note here how we get the thing you were struggling with for free now, because resolve accepts a Promise and waits for it to be resolved.

N.B. I haven’t tested this Promise implementation (although it is correct to the best of my knowledge). You should test any implementation you build against the Promises/A+ test suite (https://github.com/promises-aplus/promises-tests) and may also find the Promises/A+ spec (https://github.com/promises-aplus/promises-spec) useful in figuring out what the correct behavior is for any specific part of the algorithm. As a final resource, promise is a very minimal implementation of the Promise spec.

Leave a Comment