Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle error properly in Promise chain?

Tags:

Say we have 3 asynchronous tasks that return Promises: A, B and C. We want to chain them together (that is, for sake of clarity, taking the value returned by A and calling B with it), but also want to handle the errors correctly for each, and break out at the first failure. Currently, I see 2 ways of doing this:

A .then(passA) .then(B) .then(passB) .then(C) .then(passC) .catch(failAll) 

Here, the passX functions handle each of the success of the call to X. But in the failAll function, we'd have to handle all of the errors of A, B and C, which may be complex and not easy to read, especially if we had more than 3 async tasks. So the other way takes this into consideration:

A .then(passA, failA) .then(B) .then(passB, failB) .then(C) .then(passC, failC) .catch(failAll) 

Here, we separated out the logic of the original failAll into failA, failB and failC, which seems simple and readable, since all errors are handled right next to its source. However, this does not do what I want.

Let's see if A fails (rejected), failA must not proceed to call B, therefore must throw an exception or call reject. But both of these gets caught by failB and failC, meaning that failB and failC needs to know if we had already failed or not, presumably by keeping state (i.e. a variable).

Moreover, it seems that the more async tasks we have, either our failAll function grows in size (way 1), or more failX functions gets called (way 2). This brings me to my question:

Is there a better way to do this?

Consideration: Since exceptions in then is handled by the rejection method, should there be a Promise.throw method to actually break off the chain?

A possible duplicate, with an answer that adds more scopes inside the handlers. Aren't promises supposed to honor linear chaining of functions, and not passing functions that pass functions that pass functions?

like image 217
David Song Avatar asked Mar 28 '17 18:03

David Song


People also ask

How do you handle errors in Promise chains?

There are two ways in which you can handle errors in your promise chain, either by passing an error handler to then block or using the catch operator.

How do you handle Promise resolve?

Promise resolve() method: Any of the three things can happened: If the value is a promise then promise is returned. If the value has a “then” attached to the promise, then the returned promise will follow that “then” to till the final state. The promise fulfilled with its value will be returned.

How do you handle reject promises?

We must always add a catch() , otherwise promises will silently fail. In this case, if thePromise is rejected, the execution jumps directly to the catch() method. You can add the catch() method in the middle of two then() methods, but you will not be able to break the chain when something bad happens.

What happens if Promise is not resolved?

A promise is just an object with properties in Javascript. There's no magic to it. So failing to resolve or reject a promise just fails to ever change the state from "pending" to anything else. This doesn't cause any fundamental problem in Javascript because a promise is just a regular Javascript object.


2 Answers

You have a couple options. First, let's see if I can distill down your requirements.

  1. You want to handle the error near where it occurs so you don't have one error handler that has to sort through all the possible different errors to see what to do.

  2. When one promise fails, you want to have the ability to abort the rest of the chain.

One possibility is like this:

A().then(passA).catch(failA).then(val => {     return B(val).then(passB).catch(failB); }).then(val => {     return C(val).then(passC).catch(failC); }).then(finalVal => {     // chain done successfully here }).catch(err => {     // some error aborted the chain, may or may not need handling here     // as error may have already been handled by earlier catch }); 

Then, in each failA, failB, failC, you get the specific error for that step. If you want to abort the chain, you rethrow before the function returns. If you want the chain to continue, you just return a normal value.


The above code could also be written like this (with slightly different behavior if passB or passC throws or returns a rejected promise.

A().then(passA, failA).then(val => {     return B(val).then(passB, failB); }).then(val => {     return C(val).then(passC, failC); }).then(finalVal => {     // chain done successfully here }).catch(err => {     // some error aborted the chain, may or may not need handling here     // as error may have already been handled by earlier catch }); 

Since these are completely repetitive, you could make the whole thing be table-driven for any length of sequence too.

function runSequence(data) {     return data.reduce((p, item) => {         return p.then(item[0]).then(item[1]).catch(item[2]);     }, Promise.resolve()); }  let fns = [     [A, passA, failA],     [B, passB, failB],     [C, passC, failC] ];  runSequence(fns).then(finalVal => {     // whole sequence finished }).catch(err => {     // sequence aborted with an error }); 

Another useful point when chaining lots of promises is if you make a unique Error class for each reject error, then you can more easily switch on the type of error using instanceof in the final .catch() handler if you need to know there which step caused the aborted chain. Libraries like Bluebird, provide specific .catch() semantics for making a .catch() that catches only a particular type of error (like the way try/catch does it). You can see how Bluebird does that here: http://bluebirdjs.com/docs/api/catch.html. If you're going to handle each error right at it's own promise rejection (as in the above examples), then this is not required unless you still need to know at the final .catch() step which step caused the error.

like image 168
jfriend00 Avatar answered Sep 25 '22 00:09

jfriend00


There are two ways that I recommend (depending on what you are trying to accomplish with this):

Yes, you want to handle all errors in the promise chain with a single catch.

If you need to know which one failed, you can reject the promise with a unique message or value like this:

A .then(a => {   if(!pass) return Promise.reject('A failed');   ... }) .then(b => {   if(!pass) return Promise.reject('B failed');   ... }) .catch(err => {   // handle the error }); 

Alternatively, you can return other promises inside of .then

A .then(a => {   return B; // B is a different promise }) .then(b => {   return C; // C is another promise }) .then(c => {   // all promises were resolved   console.log("Success!")  }) .catch(err => {   // handle the error   handleError(err) }); 

In each of those promises, you will want some kind of unique error message so you know which one failed.

And since these are arrow functions we can remove the braces! Just another reason I love promises

A .then(a => B) .then(b => C) .then(c => console.log("Success!")) .catch(err => handleError(err)); 
like image 33
AJ Funk Avatar answered Sep 26 '22 00:09

AJ Funk