Writing a promise library - Part 3

Part 1 and part 2 of this series looked at how to create a simple promise class then how to make promises chainable.

The final feature to add is to have errors propagate. This will allow all errors to be caught and handled in one call to catch at the end of the promise chain. Of course we still want to be able to use catch inside the promise chain so we can deal with errors at any point.

To make the error propagate defer.reject will check if there is an error callback defined and if not will resolve the promise with the error. To prevent calling a non-error callback on an error defer.resolve will also check if the input is an error and reject the callback’s defer with the error. Similar checks have to be added to defer.resolve to keep non-error values propagating as well.

defer.bind will also check whether the callback and error callback is defined and propagate the result if necessary.

The changes from the implementation in part 2 are:

  defer.resolve = function(data) {
    var promise = this.promise;
    if (Error.prototype.isPrototypeOf(data)) {
      promise.callbackFn && promise.callbackFn.defer.reject(data);
    } else {
      promise.data = data;
      promise.status = 'resolved';
      if (!promise.callbackFn) {
        this.bind(promise);
      } else {
        promise.executeCallback(promise.callbackFn, data);
      }
    }
  };

  defer.reject = function(error) {
    var promise = this.promise;
    if (!Error.prototype.isPrototypeOf(error)) {
      promise.errorFn && promise.errorFn.defer.resolve(error);
    } else {
      promise.error = error;
      promise.status = 'rejected';
      if (!promise.errorFn) {
        this.bind(promise);
      } else {
        promise.executeCallback(promise.errorFn, error);
      }
    }
  };

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

Some example usage:

In this case the error will propagate to the catch without calling anything else in the promise chain.

readFile('doesnt_exist.txt')
  .then(function() {
    console.log("called");
  })
  .catch(function(err) {
    console.log(err);
  });
## { [Error: ENOENT, open 'doesnt_exist.txt'] errno: 34, code: 'ENOENT', path: 'doesnt_exist.txt' }

But if the error is caught the rest of the promise chain can continue.

readFile('doesnt_exist.txt')
  .catch(function(err) {
    console.log(err);
  })
  .then(function() {
    console.log("called");
  })
  .catch(function(err) {
    console.log(err);
  });
## { [Error: ENOENT, open 'doesnt_exist.txt'] errno: 34, code: 'ENOENT', path: 'doesnt_exist.txt' }
## called

Lastly, if there is no error, the resolution is propagated.

$ echo "hello world" > test.txt
readFile('test.txt')
  .catch(function(err) {
    console.log(err);
  })
  .then(function(data) {
    console.log(data);
  })
  .catch(function(err) {
    console.log(err);
  });
## hello world

Full code for this is available as a gist.