0

I want to chain calls to different libraries using promises. In case of failure, library methods return an object describing the error, but with different fields depending the library.

In order to report any error consistently to the callee, I would like to normalize all error objects to follow a common format. But I have no idea how to do that in an elegant way using Bluebird and/or the standard promise API.

In pseudo-js here is what I have :

var methodAFromLibX_Async = Promise.promisify(...);
var methodBFromLibY_Async = Promise.promisify(...);

methodAFromLibX_Async(...)
.then(function(result) {
  methodBFromLibY_Async(...)
  .then(function(result) { ... })
  .catch(normalizeAndSendErrorFromLibY);
})
.catch(normalizeAndSendErrorFromLibX);

The above code seems to work, but :

  1. I have redundant code between normalizeAndSendErrorFromLibY and normalizeAndSendErrorFromLibX
  2. I my real use case I have to chain more than 2 calls, and the pyramid shape of the code definitely starts looking like a callback hell...

EDIT: In order to be a little more clear, here the solution I envision, but can't achieve : chain with parallel path for errors & ok results

Sylvain Leroux
  • 50,096
  • 7
  • 103
  • 125
  • 1
    Is there any reason you cannot have a single `catch` at the very end of your chain where you normalize all possible errors and then "send" them? – m90 Sep 26 '16 at 09:01
  • Thank you for your comment @m90. With a global `catch`/`catchAll` only at the end, I loose the knowledge of the error origin. When errors are coming from different lib calls as explained in my example, using a bunch of `if..else` blocks I could infer the origin. But when I call several methods of the same lib in my chain, this is more complicated to get the trace back. I can't believe there isn't a more *elegant* way of doing... – Sylvain Leroux Sep 26 '16 at 09:09
  • Are you sure that is the chain you want? `normErrorFromY` handling errors from `normErrorFromX`? – Bergi Sep 26 '16 at 09:39
  • @Bergi It's time for me to take a break... Of course my initial drawing was completely wrong. This is now fixed. Sorry for the waste of time. – Sylvain Leroux Sep 26 '16 at 10:18
  • related: [Javascript Promise Pattern - differentiate error](https://stackoverflow.com/q/42015210/1048572) – Bergi Sep 07 '19 at 16:51

4 Answers4

1

If you want to avoid the pattern you are demonstrating in your example there seem to be two other options for you:

You promisify your libraries as shown, propagate errors properly in your chain and then build one function that is able to normalize all known errors:

var methodAFromLibX_Async = Promise.promisify(...);
var methodBFromLibY_Async = Promise.promisify(...);

methodAFromLibX_Async(...)
.then(function(result) {
  return methodBFromLibY_Async(...)
  .then(function(result) { ... })
})
.catch(function(err){
  if (hasLibXShape(err)){
    return Promise.reject(normalizeLibXErr(err));
  } else if (hasLibYShape(err)){
    return Promise.reject(normalizeLibYErr(err));
  }
})
.catch(function(normalizedErr){
  // handle normalized error
});

The other option would be to manually promisify your library methods and normalize the errors here:

function promisifiedMethodA(/*...args*/){
   var args = [].slice.call(arguments);
   return new Promise(function(resolve, reject){
       methodA.apply(null, args.concat([function(err, data){
          if (err) return reject(normalizeMethodAError(err));
          resolve(data);
       }]));
   });
}

Reading your latest comment I guess the latter might fit your needs better.

m90
  • 11,434
  • 13
  • 62
  • 112
  • 1
    Yes indeed, I could manually promisify my library methods and normalize the errors there. Bluebird doc insist so much in saying this is an anti-pattern, I didn't even considered that option. But for now this sounds the best solution... – Sylvain Leroux Sep 26 '16 at 09:29
1

You could use bluebird filtered catch: http://bluebirdjs.com/docs/api/catch.html#filtered-catch

And change your code to something like this:

var methodAFromLibX_Async = Promise.promisify(...);
var methodBFromLibY_Async = Promise.promisify(...);

methodAFromLibX_Async(...)
.then(function(result) {
   return methodBFromLibY_Async(...);
}).then(function(result) {
   ... 
}).catch({code: 'X_Async', message: 'bad lib X'}, function(e) {
  //If it is a methodAFromLibX_AsyncError, will end up here because

}).catch({code: 'Y_Async', message: 'bad lib Y'}, function(e) {
  //Will end up here if a was never declared at all

}).catch(function(e) {
   //Generic catch-the rest, error wasn't methodAFromLibX_AsyncError nor
   //methodBFromLibY_AsyncError
});
Anouar
  • 42
  • 1
  • 13
  • Thank you but my libs return errors as plain objects. Not instance of Error, even less from a specific *sub-class* of Error. Would you solution work in that case too ? Is there a way for factor out the common code in the catchAll clause ? – Sylvain Leroux Sep 26 '16 at 09:26
  • you can try filtering them by code or any object key. `fs.readFileAsync(...) .then(...) .catch({code: 'ENOENT'}, function(e) { console.log("file not found: " + e.path); });` – Anouar Sep 26 '16 at 09:33
1

The solution you are looking for is

return methodAFromLibX_Async(…)
.then(function(result) {
     return methodBFromLibY_Async(…)
     .then(function(result) {
          return methodCFromLibX_Async(…)
          .catch(normalizeAndThrowErrorFromLibX);
     }, normalizeAndThrowErrorFromLibY);
}, normalizeAndThrowErrorFromLibX)
.then(reportSuccess, reportError);

But this is pretty ugly. Given that your error handlers rethrow the error anyway, you might also use

return methodAFromLibX_Async(…)
.catch(normalizeAndThrowErrorFromLibX)
.then(function(result) {
     return methodBFromLibY_Async(…)
     .catch(normalizeAndThrowErrorFromLibY)
     .then(function(result) {
          return methodCFromLibX_Async(…)
          .catch(normalizeAndThrowErrorFromLibX);
     });
})
.then(reportSuccess, reportError);

which still isn't optimal. You don't want to put a .catch(normalise) on every call to these functions, and you don't want to be forced to nest them. So better factor each of them in their own function:

function withRejectionHandler(fn, normalise) {
     return function() {
         return fn.apply(this, arguments).catch(normalise);
     };
}
var methodA = withRejectionHandler(methodAFromLibX_Async, normalizeAndThrowErrorFromLibX);
var methodB = withRejectionHandler(methodBFromLibY_Async, normalizeAndThrowErrorFromLibY);
var methodA = withRejectionHandler(methodCFromLibX_Async, normalizeAndThrowErrorFromLibX);

return methodA(…).then(methodB).then(methodC).then(reportSuccess, reportError);

You might combine that with the promisification of the library methods.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • What do you think of using [`Promise.coroutine`](http://bluebirdjs.com/docs/api/promise.coroutine.html) and a generator to yield `methodA, B & C` instead of chaining promise with `.then()`. I've posted that as an [alternate solution below](http://stackoverflow.com/a/39701754/2363712) but I'm not quite sure the `.catch` handler will handle errors as I think it will do it. – Sylvain Leroux Sep 26 '16 at 11:38
  • 1
    Yes, that's totally fine and [a good solution](http://stackoverflow.com/a/28250697/1048572) if `reportSuccess` needs all the results not only the last one. The `catch` will work as expected, alternatively you can also put a `try`/`catch` around the code in your generator function. – Bergi Sep 26 '16 at 11:42
  • Thank you for having confirmed that. I do not necessary need the intermediate results here, but I will keep that option in my toolbox ! – Sylvain Leroux Sep 26 '16 at 11:54
0

As an alternate solution, using Bluebird's Promise.coroutine:

/* Bergi's solution to normalize */
function withRejectionHandler(fn, normalise) {
     return function() {
         return fn.apply(this, arguments).catch(normalise);
     };
}
var methodA = withRejectionHandler(methodAFromLibX_Async, normalizeAndThrowErrorFromLibX);
var methodB = withRejectionHandler(methodBFromLibY_Async, normalizeAndThrowErrorFromLibY);
var methodA = withRejectionHandler(methodCFromLibX_Async, normalizeAndThrowErrorFromLibX);

/* use of coroutine to sequence the work */
var workflow = Promise.coroutine(function*() {
    var resA = yield methodA(...);
    var resB = yield methodB(...);
    var resC = yield methodC(...);

    reportSuccess(resA, resB, resC);
});

workflow().catch(reportError);
Sylvain Leroux
  • 50,096
  • 7
  • 103
  • 125