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.