Writing a promise library - Part 2

In part 1 we looked at how to create a simple promise implementation that would allow converting a single node callback style function into a promise. The limitation of the implementation is that only one function can be called on resolution or rejection of the promise. Ideally we would like to chain multiple calls to then to define a sequence of calls to make as each step gets resolved.

The main change that we have to make to the previous implementation is that then needs to return a promise so that another call to then can be chained on. We’re also going to create another class, defer, which will contain a promise and handle the behavior when the promise is resolved or rejected.

To start, here is the updated promiseMaker:

var promiseMaker = function() {
  var promise = {
    callbackFn: null,
    errorFn: null,
    status: 'pending',
    type: 'promise'
  };

  promise.then = function(fn) {
    var defer = deferMaker();
    this.callbackFn = { func: fn, defer: defer };
    return defer.promise;
  };

  promise.catch = function(fn) {
    var defer = deferMaker();
    this.errorFn = { func: fn, defer: defer };
    return defer.promise;
  };

  promise.executeCallback = function(data, result) {
    var res = data.func(result);
    if (res && res.type === 'promise') {
      data.defer.bind(res);
    } else {
      data.defer.resolve(res);
    }
  };

  return promise;

};

There are a few changes compared to the previous promiseMaker. First, each instance stores a status flag which will be one of "pending", "resolved" or "rejected". The type property just lets us determine whether an object is a promise or not. This lets us quickly check each return value in the promise chain to see if it is a promise and convert it into a promise if not. The executeCallback method will invoke a callback then convert the output to a promise if necessary.

In this implementation the promise class will handle registering callbacks with then and catch and executing them. The defer class will contain the resolve and reject methods and is shown below.

var deferMaker = function() {

  var defer = {
    promise: promiseMaker()
  };

  defer.resolve = function(data) {
    if (this.promise.callbackFn) {
      this.promise.executeCallback(this.promise.callbackFn, data);
    }
  };

  defer.reject = function(error) {
    if (this.promise.errorFn) {
      this.promise.executeCallback(this.promise.errorFn, error);
    }
  };

  defer.bind = function(promise) {
    var self = this;
    if (promise.status === 'rejected') {
      promise.catch(function(err) { self.reject(err); });
    } else {
      promise.then(function(data) { self.resolve(data); });
    }
  };

  return defer;

};

The resolve and reject methods are not too different than in the previous implementation in that they invoke either the callback or the error handler. The bind method lets a promise propagate it’s result to the next defer.

And some example usage:

First promisify some node functions.

var fs = require('fs');

var readFile = function(filename) {
  var defer = deferMaker();
  fs.readFile(filename, function(err, contents) {
    if (err) {
      defer.reject(err);
    } else {
      defer.resolve(contents.toString());
    }
  });
  return defer.promise;
};

var writeFile = function(filename, data) {
  var defer = deferMaker();
  fs.writeFile(filename, data, function(err) {
    if (err) {
      defer.reject(err);
    } else {
      defer.resolve(filename);
    }
  });
  return defer.promise;
};
$ echo "hello world" > test.txt

Then chain together a few promises.

readFile('test.txt')
  .then(function(data) {
    return writeFile('output.txt', data);
  })
  .then(function(data) {
    console.log('wrote to', data);
  });
## wrote to output.txt

The next part of this series will deal with propagating errors so that one call to catch can be used to handle errors anywhere in the promise chain. See that in part 3.