3

Suppose I have a promise as following:

p.then(Task1)
 .then(Task2)
 .then(Task3)
 .catch(errorHandler);

When Task2 encounters error, how do I know the error is from Task2 in catch?

Tony
  • 1,019
  • 2
  • 13
  • 25
  • In Task2 you can do `try{...code} catch(err){return Promise.reject("rejected at task 2: " + err)}` – Redu Aug 30 '16 at 09:38
  • 3
    easiest way is to make each Task throw a meaningful error – Jaromanda X Aug 30 '16 at 10:05
  • @Redu try/catch only works for synchronous errors. I gather the OP wants to catch asynchronous errors from Task2 as well. – jib Aug 30 '16 at 12:04
  • 1
    @jib asynchronous errors are very easy. You just make your asynchronous call and return the returned promise and handle the result or the error at the next then stage's `fulfilled` or `rejected` callback. Any result or error shall be returned to the next then stage for handling. I guess there can be three things going wrong. 1) an uncontrolled synchronous error (try catch and reject from catch block) 2) an API returns promise to be rejected.. just return it to the next then stage, 3) controlled error (like insufficient data or missing argument) return a `Promise.reject("withADescription")` – Redu Aug 30 '16 at 12:18
  • 1
    How would you do this in synchronous code? – Benjamin Gruenbaum Aug 31 '16 at 09:00

5 Answers5

2

everyone! I had researched demonstrated code by myself.

I hoped everyone can review my answer, it's good or not.


Introduction:

It shows how to trace promise in each handler, used customized error handler to catch error. To understand the workflow of promise.

You can copy the following demonstrated code and paste in your node.js. According to the example and log message, it's good for the developers to learn promise.


The used promise module is as following:

  • bluebird

The demonstrated code is as following:

var Promise = require('bluebird');

// You can input any argument in this function to test workflow of promise 
function testPromise(input) {
    let promise = Promise.resolve(input);
    promise
      .then(makeTask('Task1'))
      .then(makeTask('Task2'))
      .then(makeTask('Task3'))
      .catch(makeErrorPredicate('Task1'), taskLog('Task1'))
      .catch(makeErrorPredicate('Task2'), taskLog('Task2'))
      .catch(makeErrorPredicate('Task3'), taskLog('Task3'))
}

// define task handler functions
function makeTask(task) {
    return function task1Handler(input) {
        if (input === task) {
            throw new Error(task)
        }            
        return input
    }
}

// custom error that it checks error message 
function makeErrorPredicate(msg) {
    return function taskError(err) {
        var result = err.message === msg;
        console.log(msg + ': ' + result)
        return result;
    }
}

// hint the error when the error has matched
function taskLog(msg) {
    return function thelog(err) {
        console.log('It\'s ' + msg)
    }  
}

The example:

>testPromise('Task1')
Task1: true
It's Task1

>testPromise('Task2')
Task1: false
Task2: true
It's Task2

>testPromise('Task3')
Task1: false
Task2: false
Task3: true
It's Task3

From the example above we can know:

When input is 'Task1', the route is:

firstHandler -> firstCatcher

When input is 'Task2', the route is:

firstHandler -> secondHandler -> firstCatcher -> secondCather

When input is 'Task3', the route is:

firstHandler -> secondHandler -> thirdHandler -> firstCatcher -> secondCatcher -> thirdCatcher

So, from the result above we know, we can understand the promise how to work.


If everyone is happy with this answer or not, please let me know, thanks.

Community
  • 1
  • 1
Tony
  • 1,019
  • 2
  • 13
  • 25
  • 1
    `makeTaskHandler` should be named `makeTask` or `makeThrowTask`, and `makeTaskError` should be named `makeErrorPredicate` or `makeMessageMatcher` or something. – Bergi Aug 31 '16 at 08:21
  • @Bergi I agree your suggestion! – Tony Aug 31 '16 at 08:39
1

Since a promise chain cannot hold this information, you need to store it somewhere. Two solutions:

Decorating the error:

p.then(Task1)
 .then(function() {
    try {
      Task2();
    } catch (e) {
      e.isFromTask2 = true;
      throw e;
    } 
  })
 .then(Task3)
 .catch(errorHandler); // now you can check err.isFromTask2

Using a scope for the promise chain:

{
  let isFromTask2 = false;
  p.then(Task1)
   .then(function() {
      try {
        Task2();
      } catch (e) {
        isFromTask2 = true;
        throw e;
      } 
    })
   .then(Task3)
   .catch(errorHandler); // now you can check isFromTask2
}
solendil
  • 8,432
  • 4
  • 28
  • 29
  • `try`/`catch` doesn't work when `Task2` is asynchronous. See Redu's and Jib's answers below. – Bergi Sep 01 '16 at 22:47
0

In order to know an (asynchronous) error in the final catch came from Task2 in all browsers, you can catch it, tag it, and rethrow it:

var tag = (e, name) => (e.isFrom = name, Promise.reject(e));

p.then(Task1)
 .then(Task2)
 .catch(e => tag(e, "Task2"))
 .then(Task3)
 .catch(errorHandler);

Unfortunately, this will catch errors from Task1 and p as well, so you also need a "catch-bypass":

p.then(Task1)
 .then(result => Task2(result).catch(e => tag(e, "Task2")))
 .then(Task3)
 .catch(errorHandler);

This makes any error from Task1 or earlier "go around" (i.e. bypass) our catch of Task2.

This works because we put the catch inside the success callback of .then. Every .then has an implicit second argument which is an error handler, much like with .catch, except it doesn't catch the corresponding success callback, in this case Task2 and our catch, only previous errors on the chain (i.e. errors from Task1 or any previous step).

Omitting an error callback means "pass this through unchanged". i.e. same as e => Promise.reject(e) here.

let tag = (e, name) => (e.isFrom = name, Promise.reject(e));

let a = () => Promise.resolve();
let b = () => Promise.reject(new Error("Fail"));
let c = () => Promise.resolve();
let x = () => null.f();

let foo = failFirstStep => Promise.resolve()
.then(failFirstStep? x : a)
.then(() => b().catch(e => tag(e, "b")))
.then(c)
.catch(e => console.log((e.isFrom || "something else") + " failed"));

foo(false).then(() => foo(true));

To generalize, you could tag functions instead, but I find it hard to do in a way that's idiomatic and doesn't interfere with readability or timing, so I'm going to leave that as an exercise. In practice, there's a better answer, but I find this technique interesting enough to cover, and useful in certain cases.

Community
  • 1
  • 1
jib
  • 40,579
  • 17
  • 100
  • 158
  • 1
    The first would also have caught errors from `p`, not only `Task1`. – Bergi Aug 31 '16 at 08:22
  • Passing `0` is as good as passing nothing (or any non-function argument). You should omit that, it's the standard behaviour, and `0` will be confusing the heck out everyone reading this code. – Bergi Aug 31 '16 at 08:24
  • Protip: `const tag = name => e => Promise.reject(e.tag = name, e);` will let you use simply `.catch(tag("TaskN"))` – Bergi Aug 31 '16 at 08:26
  • @Bergi True, though I try to avoid the `.then(foo())` pattern because of how much it looks like a common bug, but that may just be me. – jib Aug 31 '16 at 13:18
  • No, it's not just you :-) Naming can ease it though, `tagging` or `withTag` or `makeTagger` (or any other scheme you devise) might hint that the call does return another function. – Bergi Aug 31 '16 at 13:25
  • @Bergi thanks for the info on `0`, never realized this was default behavior before. Sweet. Edited. – jib Aug 31 '16 at 14:58
0

Most browsers support the error.stack property, and some modern browsers even support asynchronous callstacks:

let a = () => Promise.resolve();
let b = () => Promise.reject(new Error("Fail"));
let c = () => Promise.resolve();

Promise.resolve().then(a).then(b).then(c).catch(e => console.log(e.stack));

In Firefox 48 it outputs: b@http://stacksnippets.net/js:14:30.

In Chrome 52 it outputs: Error: Fail at b (http://stacksnippets.net/js:14:30).

This is the most idiomatic solution, as it doesn't interfere with how code is written. Unfortunately, not all browsers support asynchronous callstacks yet, and the output varies a bit from browser to browser, as it's meant for debugging.

jib
  • 40,579
  • 17
  • 100
  • 158
  • This answers the question "How do I know where the error comes from" quite literally, but I suspect the OP wants to know "…and how do I handle it depending on the source", for which stack traces are not so helpful. – Bergi Aug 31 '16 at 08:28
  • error.stack isn't really supported at all browsers - and this will probably fail in minified code. – Benjamin Gruenbaum Aug 31 '16 at 09:00
  • @BenjaminGruenbaum Seems supported by most desktop browsers at least, from the compatibility table in the link. – jib Aug 31 '16 at 13:21
-1

You may do like this;

Promise.resolve(21).then(v => v*2)
                   .then(v => {
                                try { throw new Error(v + " is immutable") } // assuming that your code results an error
                                catch(err){ return Promise.reject({message: "2nd then stage threw an error", err:err}) }
                              })
                   .then(v => v)
                   .catch(err => console.log(err.message, "\n",err.err.message));
Redu
  • 25,060
  • 6
  • 56
  • 76
  • `Task2` was mentioned (I guess) as an example, what if they want to determine in which of the three tasks the error occurred? – robertklep Aug 30 '16 at 11:37
  • @robertklep If it matters for you to know at which `then` stage you've got an uncontrolled error then in each `fulfillment` callback you should run your code in a `try` block and capture any possible errors by the succeeding `catch` block just like i have shown. Once you have caught the error then you can safely return a rejected promise with all the necessary data and information. – Redu Aug 30 '16 at 11:44
  • 1
    Using `try/catch` wouldn't work if the task was performing an async operation (which is fair to assume). If the task itself would be using promises too, `function Task2() { return SomeTask().catch(...) }` might be a better solution. – robertklep Aug 30 '16 at 11:54
  • Well exactly. The idea here is to not let go the error. However as per your assumption, if we are dealing with a promisified asynchronous API at the `then` stage, the rightful thing would be to return the received promise and handle the result or the error at the next then stage's `fulfillment` or `rejected` callbacks. This way we can always be able to tell at which then stage the error had happened and also avoid nested `then`s. So it's best to have `rejected` callbacks (2nd callback) in each `then` stage. – Redu Aug 30 '16 at 12:08
  • @Redu "*handle the error at the next then stage's rejected callbacks.*" doesn't really work. A rejected callback (whether `then` or `catch`) handles *all* errors from *any* of the previous promises in the chain, not only that of the prior stage. – Bergi Aug 31 '16 at 08:33
  • @Bergi Yes that's totally right and that's the reason why i said "So it's best to have rejected callbacks (2nd callback) in each `then` stage" in my previous comment. – Redu Aug 31 '16 at 08:43
  • @Redu: I rather was disagreeing with the sentence in between: "*This way we can always be able to tell at which then stage the error had happened and also avoid nested thens.*" – Bergi Aug 31 '16 at 08:45
  • @Bergi If every single `then` stage has it's own `onrejected` handler (or a succeeding `catch` stage) one can perfectly pinpoint the error's source... including the uncaught ones a `try` and `catch` block is adapted like in my answer. But this is a very primitive knowledge and i am sure you have some other point which i can not follow. May be i should have switched the order of the sentences like "So it's best to have rejected callbacks in each then stage; this way we can always be able to tell at which then stage the error had happened." Is this it..? – Redu Aug 31 '16 at 08:57
  • @Redu: Yeah, but exceptions from inside an error handler are possible as well - and sometimes even necessary, when you want to rethrow an error because you cannot really *handle* it. that's why flat `then` chains don't work well. – Bergi Aug 31 '16 at 09:02
  • @Bergi Hmm.. May be, but i can not think of any condition to arise that necessity. My choice would be to avoid nested `then`s at any cost though. Worst case i would post it to next `then`'s `onrejected` handler by a `return Promise.reject("noNestingAllowed")` But again; you might be right under some conditions. I would love to see an example of that. – Redu Aug 31 '16 at 09:25
  • @Redu: `Promise.reject(new Error("0")).catch(function(err){throw new Error(err.message+"a")}).then(Task1, function(err){throw new Error(err.message+"b")}).then(Task2, function(err){throw new Error(err.message+"c")})` - when task 0 rejects, you suddenly end up with "0abc" not "0a". – Bergi Aug 31 '16 at 10:06
  • @Bergi This discussion might go on forever but one can always `try` & `catch` in the `onrejected` handler as well and return a `Promise.resolve(err)` to avoid the successive promise API `catch`es (`onrejected` handlers) to be called. On the other hand the one pseudo nested `then` usage that i can think of could be a recursive `tryNTimes` before calling `reject` solution. – Redu Aug 31 '16 at 14:29
  • @Redu a nested `.then` is a branch, a specific behavior that is actually desired here. Not to be confused with [the promise constructor anti-pattern](http://stackoverflow.com/q/23803743/918910) which is about `.then` inside promise constructors, which, unlike this, *is* an anti-pattern. – jib Aug 31 '16 at 15:04