How to normalize error objects in a promise chain?

2020-02-16 05:19发布

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

4条回答
ゆ 、 Hurt°
2楼-- · 2020-02-16 05:48

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
});
查看更多
劳资没心,怎么记你
3楼-- · 2020-02-16 05:58

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.

查看更多
兄弟一词,经得起流年.
4楼-- · 2020-02-16 06:12

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.

查看更多
甜甜的少女心
5楼-- · 2020-02-16 06:12

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);
查看更多
登录 后发表回答