Skip to content

Instantly share code, notes, and snippets.

@jackcviers
Last active December 21, 2015 15:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jackcviers/6326928 to your computer and use it in GitHub Desktop.
Save jackcviers/6326928 to your computer and use it in GitHub Desktop.
Make Promises. Even the ones you can't keep.
In "Make No Promises" David Nolen asserts that promises don't eliminate callback hell[1]. This is not true. Take a situation where a program makes several asynchronous calls and aggregates the results in some processing step. Each call may fail. If any one of the calls fails the program cannot continue processing and must report the error.
It would be possible to write the program in a traditional callback style, and make the code easy to understand and follow. I can think of two ways to do this.
The first is to make each call and define a success handler that added the results to a shared array, and an error handler that reported each error. Then pass the success and error handler to each call. This leads to a possible outcome where the resulting aggregation may be partially complete. The program must keep track of how many of the requests have completed. The program must check the length of the results on each completion and error to see if processing can continue. I dislike this approach, because the results are in a possibly indeterminate state. The program must make additional explicit assertions at each step. The program must modify variables external to the control flow provided by the callbacks so that it can handle processing on call complete and error reporting if there are errors.
The second way is to make each call dependent upon another call, and thread the error and result aggregation through several callbacks. This program is likely to become very complex, and will indeed look like callback hell to the reader. Additionally, the calls must happen in sequential order, since each call except the first depends upon the result of the preceding call. The resulting continuation pattern is resistant to change, and mildly difficult to reason about. It will not explicitly modify variables outside of the various callbacks, but it also becomes synchronously blocking.
Promises, especially the promise implementation provided by when.js[2] provide a third and cleaner option. It is presented below. All of the possible results are wrapped in a promise. The first step is creating several deferred objects. Each deferred has a resolver that can be in one of three states: pending, resolved, or error. Each deferred also has a promise, which can be in one of the same three states. All of the promises are collected, before resolution, into a single array of promises. All of the calls are made without respect to the other calls. Each call is made asynchronously, and no one call can block the other while the result of a call is pending. The array of promises is then passed to when.all, which will aggregate the returns of the calls in a promise of an array of resolved promises of results. The resulting promise resolves or errors only when all the promises passed to it are resolved or one of the promises goes into an error state. The resulting promise has a method, then, which takes three functions as parameters. The first is a success handler that takes an array of results. The second is an error handler that takes an error. The third, optional parameter is a function that may be called many times -- once for each promise resolution or error that was passed to when.all to construct the aggregate promise. This is typically used to report asynchronous progress. The error state is handled without explicitly threading partial results through intermediate processing steps, and success processing is handled all at once as well.
Although I didn't show it in this example, the return of the success or error handler is also a promise, which can be passed to a further then in a continuous chain of logical steps.
When handles several callback patterns. However, it doesn't handle the callback pattern provided by request[3], which uses a single callback for both success and failure, passing nulls to indicate error/success state. when provides for extension via its when/node/function.callback module. I named it m and provide a cb utility for use with it in the program below.
I think the resulting code is legible and easy to reason about, and certainly avoids callback hell.
[1] http://swannodette.github.io/2013/08/23/make-no-promises/
[2] https://github.com/cujojs/when
[3] https://github.com/mikeal/request
var when = require('when');
var m = require('when/node/function');
var request = require('request');
function processAllTheJsons(returns){
console.log('Great! do something.');
}
function errAllTheThings(err){
console.log('We didn\'t get all the things. :-( ');
console.log('Here\'s what went wrong:');
console.log(err);
}
var deferred1 = when.defer();
var deferred2 = when.defer();
var deferred3 = when.defer();
var deferred4 = when.defer();
var deferred5 = when.defer();
var deferred6 = when.defer();
var promises = [deferred1.promise, deferred2.promise, deferred3.promise, deferred4.promise, deferred5.promise, deferred6.promise];
var cb = function(callback) {
return function(error, response, body) {
if(error) {
callback(error);
} else if(!error && response.statusCode !== 200) {
callback(new UploadRequestError(response.statusCode, body));
} else {
callback(null, {response: response, body: body});
}
};
};
var deferredcb1 = m.createCallback(deferred1.resolver);
var deferredcb2 = m.createCallback(deferred2.resolver);
var deferredcb3 = m.createCallback(deferred3.resolver);
var deferredcb4 = m.createCallback(deferred4.resolver);
var deferredcb5 = m.createCallback(deferred5.resolver);
var deferredcb6 = m.createCallback(deferred6.resolver);
when.all(promises).then(processAllTheThings, errAllTheThings);
var request1 = request.get({url:'/mine/tunnels'}, cb(deferred1));
var request2 = request.get({url:'/mine/miners'}, cb(deferred2));
var request3 = request.get({url:'/mine/carts'}, cb(deferred3));
var request4 = request.get({url:'/mine/tools'}, cb(deferred4));
var request5 = request.get({url:'/mine/foremen'}, cb(deferred5));
var request6 = request.get({url:'/mine/elevators'}, cb(deferred6));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment