67

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?

7
  • 2
    Why not return Promise.reject()?
    – elclanrs
    Commented Aug 16, 2015 at 7:02
  • 2
    Return a rejected Promise from the then block will make the then block return a rejected Promise which propagates through the Promise chain until it is caught.
    – lixiang
    Commented Aug 16, 2015 at 18:51
  • Updated my answer based on your clarification.
    – rrowland
    Commented Aug 16, 2015 at 19:29
  • 1
    @lixiang: Yes, in generators (or with async/await) you can return and throw from catch blocks, there are no callbacks
    – Bergi
    Commented Aug 17, 2015 at 6:47
  • 3
    It's been a while since I posted this question. I myself had completely switched to async/await. The inability of returning from a promise just makes it a bad interface for asynchronous programming in general.
    – lixiang
    Commented May 9, 2017 at 20:47

4 Answers 4

79

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);

Here Promise.resolve() plays an important role. Without it, Function1().catch(errorHandler1) would be in the main chain the catch() would not be insulated from the main chain.

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.
7
  • Thank you for this comprehensive answer! I have marked your response as the answer. However, as I understand the issue further, I think trying to return from the catch block or skipping a Promise chain should not be abused. A "then" block in a Promise chain represents one step of the whole process. If we want to treat it as an individual statement which throws unique error, we'd better not use a Promise chain at all
    – lixiang
    Commented Aug 20, 2015 at 21:18
  • 1
    There's no abuse. Patterns like those above are commonplace and very necessary for async flow control. Commented Aug 20, 2015 at 22:09
  • Great answer! But I think your first fiddle has an issue with the implementation of myCustomError that it's making it behave like any other Error (to see it just uncomment the reject(new Error(...). So I think line 5 should be: myCustomError.prototype = Error; Commented May 15, 2017 at 19:13
  • But I think this is the proper way for browsers with Object.create: myCustomError.prototype = Object.create(Error.prototype); myCustomError.prototype.constructor = myCustomError; Commented May 15, 2017 at 19:35
  • 1
    @MarianoDesanze, many thanks for spotting that. My first Demo now uses a version of MyCustomError() that allows instanceof to distinguish between error types. To everyone else - this edit doesn't change the answer materially, but it does make the first demo more convincing. Commented May 18, 2017 at 14:11
15

First off, I see a common mistake in this section of code that could be completely confusing you. This is your sample code block:

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

You need pass function references to a .then() handler, not actually call the function and pass their return result. So, this above code should probably be this:

Promise.resolve(someFunction()).then(function() {
  console.log('someFunction should throw error');
  return -2;
}).catch(function(err) {
   if (err instanceof MyCustomError) {
     // returning a normal value here will take care of the rejection
     // and continue subsequent processing
     return -1;
   }
}).then(someOtherFunction);    // just pass function reference here

Note that I've removed () after the functions in the .then() handler so you are just passing the function reference, not immediately calling the function. This will allow the promise infrastructure to decide whether to call the promise in the future or not. If you were making this mistake, it will totally throw you off for how the promises are working because things will get called regardless.


Three simple rules about catching rejections.

  1. If nobody catches the rejection, it stops the promise chain immediately and the original rejection becomes the final state of the promise. No subsequent handlers are invoked.
  2. If the promise rejection is caught and either nothing is returned or any normal value is returned from the reject handler, then the reject is considered handled and the promise chain continues and subsequent handlers are invoked. Whatever you return from the reject handler becomes the current value of the promise and it as if the reject never happened (except this level of resolve handler was not called - the reject handler was called instead).
  3. If the promise reject is caught and you either throw an error from the reject handler or you return a rejected promise, then all resolve handlers are skipped until the next reject handler in the chain. If there are no reject handlers, then the promise chain is stopped and the newly minted error becomes the final state of the promise.

You can see a couple examples in this jsFiddle where it shows three situations:

  1. Returning a regular value from a reject handler, causes the next .then() resolve handler to be called (e.g. normal processing continues),

  2. Throwing in a reject handler causes normal resolve processing to stop and all resolve handlers are skipped until you get to a reject handler or the end of the chain. This is effective way to stop the chain if an unexpected error is found in a resolve handler (which I think is your question).

  3. Not having a reject handler present causes normal resolve processing to stop and all resolve handlers are skipped until you get to a reject handler or the end of the chain.

9
  • Thanks for the catch, I change it! Can you take a look at my extended question?
    – lixiang
    Commented Aug 16, 2015 at 19:09
  • @lixiang - you can't use return to stop all further processing in promises. You just can't because of their async nature so there really isn't an exact analogy. I've outlined the general options you can use with promises. If all your downstream .catch() handlers check the type of the error and rethrow it if it isn't something they are specifically handling, then you can throw a unique error that nobody else will handle and it will stop all further processing. It will go through all the downstream .catch() handlers, but get rethrown by each to eventually get to the end of the chain.
    – jfriend00
    Commented Aug 16, 2015 at 19:15
  • @lixiang - you could even just create your own convention of an abort class of error (a subclass of Error) that no mid-stream .catch() handler is ever supposed to catch without rethrowing. But, promises don't have such a thing built-in.
    – jfriend00
    Commented Aug 16, 2015 at 19:17
  • Can you provide some pointers as to why this is not possible with Promises?
    – lixiang
    Commented Aug 16, 2015 at 22:29
  • 1
    @jfriend00, springs can unwind, not stacks Commented Oct 21, 2016 at 9:36
4

There is no built-in functionality to skip the entirety of the remaining chain as you're requesting. However, you could imitate this behavior by throwing a certain error through each catch:

doSomething()
  .then(func1).catch(handleError)
  .then(func2).catch(handleError)
  .then(func3).catch(handleError);

function handleError(reason) {
  if (reason instanceof criticalError) {
    throw reason;
  }

  console.info(reason);
}

If any of the catch blocks caught a criticalError they would skip straight to the end and throw the error. Any other error would be console logged and before continuing to the next .then block.

0

If you can use the newer async await this is pretty simple to implement:

async function myfunc() {

  try {
     return await anotherAsyncFunction();
  } catch {
    //do error handling
 
    // can be async or not.
    return errorObjct();
    
  }

}

let alwaysGetAValue = await myfunc();

Depending on what technology your using you may need some kind of high level wrapper function to allow for the top level await.

Not the answer you're looking for? Browse other questions tagged or ask your own question.