lixiang lixiang - 14 days ago 7
Javascript Question

How to return from a Promise's catch/then block

There are many tutorials on how to use "then" and "catch" while programming with JavaScript Promise. However, all these tutorials seem to miss an important point: returning from a then/catch block to break the Promise chain. Let's start with some synchronous code to illustrate this problem:

try {
someFunction();
} catch (err) {
if (!(err instanceof MyCustomError))
return -1;
}
someOtherFunction();


In essence, I am testing a caught error and if it's not the error I expect I will return to the caller otherwise the program continues. However, this logic will not work with Promise:

Promise.resolve(someFunction).then(function() {
console.log('someFunction should throw error');
return -2;
}).catch(function(err) {
if (err instanceof MyCustomError) {
return -1;
}
}).then(someOtherFunction);


This logic is used for some of my unit tests where I want a function to fail in a certain way. Even if I change the catch to a then block I am still not able to break a series of chained Promises because whatever is returned from the then/catch block will become a Promise that propagates along the chain.

I wonder if Promise is able to achieve this logic; if not, why? It's very strange to me that a Promise chain can never be broken. Thanks!

Edit on 08/16/2015:
According to the answers given so far, a rejected Promise returned by the then block will propagate through the Promise chain and skip all subsequent then blocks until is is caught (handled). This behavior is well understood because it simply mimics the following synchronous code (approach 1):

try {
Function1();
Function2();
Function3();
Function4();
} catch (err) {
// Assuming this err is thrown in Function1; Function2, Function3 and Function4 will not be executed
console.log(err);
}


However, what I was asking is the following scenario in synchronous code (approach 2):

try {
Function1();
} catch(err) {
console.log(err); // Function1's error
return -1; // return immediately
}
try {
Function2();
} catch(err) {
console.log(err);
}
try {
Function3();
} catch(err) {
console.log(err);
}
try {
Function4();
} catch(err) {
console.log(err);
}


I would like to deal with errors raised in different functions differently. It's possible that I catch all the errors in one catch block as illustrated in approach 1. But that way I have to make a big switch statement inside the catch block to differentiate different errors; moreover, if the errors thrown by different functions do not have a common switchable attribute I won't be able to use the switch statement at all; under such a situation, I have to use a separate try/catch block for each function call. Approach 2 sometimes is the only option. Does Promise not support this approach with its then/catch statement?

Answer

This can't be achieved with features of the language. However, pattern-based solutions are available.

Here are two solutions.

Rethrow previous error

This pattern is basically sound ...

Promise.resolve()
.then(Function1).catch(errorHandler1)
.then(Function2).catch(errorHandler2)
.then(Function3).catch(errorHandler3)
.then(Function4).catch(errorHandler4)
.catch(finalErrorHandler);

Promise.resolve() is not strictly necessary but allows all the .then().catch() lines to be of the same pattern, and the whole expression is easier on the eye.

... but :

  • if an errorHandler returns a result, then the chain will progress to the next line's success handler.
  • if an errorHandler throws, then the chain will progress to the next line's error handler.

The desired jump out of the chain won't happen unless the error handlers are written such that they can distinguish between a previously thrown error and a freshly thrown error. For example :

function errorHandler1(error) {
    if (error instanceof MyCustomError) { // <<<<<<< test for previously thrown error 
        throw error;
    } else {
        // do errorHandler1 stuff then
        // return a result or 
        // throw new MyCustomError() or 
        // throw new Error(), new RangeError() etc. or some other type of custom error.
    }
}

Now :

  • if an errorHandler returns a result, then the chain will progress to the next FunctionN.
  • if an errorHandler throws a MyCustomError, then it will be repeatedly rethrown down the chain and caught by the first error handler that does not conform to the if(error instanceof MyCustomError) protocol (eg a final .catch()).
  • if an errorHandler throws any other type of error, then the chain will progress to the next catch.

This pattern would be useful if you need the flexibility to skip to end of chain or not, depending on the type of error thrown. Rare circumstances I expect.

DEMO

Insulated Catches

Another solution is to introduce a mechanism to keep each .catch(errorHandlerN) "insulated" such that it will catch only errors arising from its corresponding FunctionN, not from any preceding errors.

This can be achieved by having in the main chain only success handlers, each comprising an anonymous function containing a subchain.

Promise.resolve()
.then(function() { return Function1().catch(errorHandler1); })
.then(function() { return Function2().catch(errorHandler2); })
.then(function() { return Function3().catch(errorHandler3); })
.then(function() { return Function4().catch(errorHandler4); })
.catch(finalErrorHandler);

Again Promise.resolve() makes the expression easier on the eye

Now,

  • if an errorHandler returns a result, then the chain will progress to the next line.
  • if an errorHandler throws anything it likes, then the chain will progress directly to the finalErrorHandler.

Use this pattern if you want always to skip to the end of chain regardless of the type of error thrown. A custom error constructor is not required and the error handlers do not need to be written in a special way.

DEMO

Usage cases

Which pattern to choose will determined by the considerations already given but also possibly by the nature of your project team.

  • One-person team - you write everything and understand the issues - if you are free to choose, then run with your personal preference.
  • Multi-person team - one person writes the master chain and various others write the functions and their error handlers - if you can, opt for Insulated Catches - with everything under control of the master chain, you don't need to enforce the discipline of writing the error handlers in that certain way.