Promise paths

Promise chain looks like a railroad with two tracks.

You can chain multiple steps to each promise. You can also fork into parallel paths for a step: successful and failed, then continue again. Let us look at the use cases.

Assume we make an http request, then work with results. Here is the http call mock

var Q = require('q');
function http() {
  return Q({
    status: 200,
    data: {
      foo: ['b', 'a', 'r']
    }
  });
}

A typical Ajax response resolves with an object. The response has status and data fields.

Often we start with this:

http()
  .then(function (response) {
    console.log(response.data.foo);
  });
// ['b', 'a', 'r']

Let us handle server error. We can also throw an error from http mock to simulate server error

function http() {
  throw new Error('server error');
}
Q.fcall(http)
  .then(function (response) {
    console.log(response.data.foo); // 1
  }, function (rejected) {
    console.error('http:', rejected.message);
  })
  .done();
// http: server error

I am using Q.fcall to convert an exception in http function into a rejected promise.

Notice that we start handling data from response right away (line // 1). Let us separate getting the response from printing the data.

http()
  .then(function (response) {
    return response.data; // 1
  }, function (rejected) {
    console.log(rejected.message);
  })
  .then(function (data) { // 2
    console.log(data.foo);
  })
  .done();
// ['b', 'a', 'r']

Any value returned from the promise (// 1) becomes the value provided to the next step (// 2). Now we check the received data object to make sure the server has sent what we expect:

require('lazy-ass');
var check = require('check-types');
http()
  .then(function (response) {
    return response.data;
  }, function (rejected) {
    console.log(rejected.message);
  })
  .then(function checkDataFormat(data) {
    lazyAss(check.object(data));
    lazyAss(check.array(data.foo));
    return data;
  })
  .then(function useResponse(data) {
    console.log(data.foo);
  })
  .done();
// ['b', 'a', 'r']

Since we can fail data check (in checkDataFormat), we must add error handling function

// http returns { data: 'nothing' }
http()
  .then(function onSuccess(response) {
    return response.data;
  }, function onFail(rejected) {
    console.error(rejected.message);
  })
  .then(function checkDataFormat(data) {
    lazyAss(check.object(data), 'data should be an object');
    lazyAss(check.array(data.foo), 'data should have foo array');
    return data;
  })
  .then(function useResponse(data) {
    console.log(data.foo);
  }, function badData(rejected) {
    console.error(rejected.message);
  })
  .done();
// data should have foo array

The logical links in this chain look like this

http -> onSuccess ->  | checkDataFormat -> useResponse -> done
  \                 / |               \                /
    -> onFail -->     |                 -> badData -->
                      |
   http / ajax layer  |       application layer

We can think of first fork onSuccess / onFail as handling the http communication. This part depends on the framework used (jQuery or Angular for example), while the right half checkDataFormat / useResponse / badData is application specific.

We can usually separate these two into different modules, making the service (http) a promise-returning function.

var Server = {
  getData: function () {
    return http()
      .then(function onSuccess(response) {
        return response.data;
      }, function onFail(rejected) {
        console.error(rejected.message);
      });
  }
};
// Application
function checkDataFormat(data) {
  lazyAss(check.object(data), 'data should be an object');
  lazyAss(check.array(data.foo), 'data should have foo array');
  return data;
}
function showData() {
  Server.getData()
    .then(checkDataFormat)
    .then(function useResponse(data) {
      console.log(data.foo);
    }, function badData(rejected) {
      console.error(rejected.message);
    })
    .done();
}

When using multiple forks like this it is important to truly propagate the error. Right now, if the server returns an error, onFail only shows the error message, but does not throw an Error. Thus the promise continues on success path!

Q.fcall(function () { throw new Error('Failed!'); })
  .then(null, function onError(err) {
    console.error(err.message);
  })
  .then(function (data) {
    console.log('data =', data);
  })
  .done();
// Failed!
// data = undefined

This fact leads to the following advice:

Throw new exception from the reject callback if you want to continue on the error path. Otherwise, handle the error and return the result.

We can update our server example: the http server can print / log the error and then rethrow the error to let a downstream link handle it.

http()
  .then(function (response) {
    return response.data;
  }, function (rejected) {
    console.error('http:', rejected.message);
    throw new Error(rejected.message);
  })
  .then(function checkDataFormat(data) {
    lazyAss(check.object(data), 'data should be an object');
    lazyAss(check.array(data.foo), 'data should have foo array');
    return data;
  })
  .then(function useResponse(data) {
    console.log(data.foo);
  }, function badData(rejected) {
    console.error('data error:', rejected.message);
  })
  .done();
// http: server error
// data error: server error

This is equivalent to the following diagram

http -> onSuccess ->  | checkDataFormat -> useResponse -> done
  \                   |               \                /
    -> onFail ---------------------------> badData -->
                      |
   http / ajax layer  |       application layer

onFail callback does not handle the problem, so it throws an error. If it could handle the error (for example by providing default data or using cached result), it should just return it, then it would continue on success path.

You can rethrow the same rejection reason, or you can throw new Error by wrapping the previous one. I recommend verror for uniform error wrapping

var VError = require('verror');
// server code as before
// Application
function showData() {
  Server.getData()
    .then(checkDataFormat)
    .then(function useResponse(data) {
      console.log(data.foo);
    }, function badData(rejected) {
      throw new VError(rejected, 'cannot show data');
    })
    .done();
}
// throws error that has .causes field pointing at previous error

Conclusion

Once you start adding success / failure forks to promise chain, it starts looking like a railroad with two tracks. You can always switch from success to failure track by throwing an exception. When you are on the failure path, you MUST throw an error to continue down the failure track. Otherwise you will switch to the success callback on the next link.