J. A. Johns J. A. Johns - 6 months ago 212
Javascript Question

Bluebird (or other Promise library) Keep Promise Error Stack Traces

Okay, I may be just missing the obvious, but I can't seem to find the general answer to this, and my Google-Fu has so far failed me.

In a Catch handler of a Promise, how do you re-throw the error, while still keeping the Promise stack trace of the original error?

That perhaps is not the correct description, so here is an example:

https://jsfiddle.net/8sgj8x4L/19/

With this code, the traced stack is:

Warning: a promise was rejected with a non-error: [object String]
at RejectWithAnError (https://fiddle.jshell.net/_display/:51:19)
at https://fiddle.jshell.net/_display/:57:14
From previous event:
at StartTheProgram (https://fiddle.jshell.net/_display/:56:6)
at window.onload (https://fiddle.jshell.net/_display/:67:1)
bluebird.js:1444 Unhandled rejection an error occurred


However, if a catch handler is added, and the error is re-rejected or re-thrown from that handler, then the stack becomes the location of the new Reject method call:

https://jsfiddle.net/8sgj8x4L/18/

Which traces this stack trace:

Warning: a promise was rejected with a non-error: [object String]
at https://fiddle.jshell.net/_display/:65:23
From previous event:
at StartTheProgram (https://fiddle.jshell.net/_display/:61:11)
at window.onload (https://fiddle.jshell.net/_display/:70:1)
bluebird.js:1444 Unhandled rejection an error occurred


You can see the inner method which dispatched the original error, "RejectWithAnError", disappears from the second stack, as the error was caught and re-thrown.

For reference, here is the complete code from the JSFiddle (The newest Bluebird is referenced as an external dependency):

window.P.longStackTraces();

function RejectWithAnError() {
var err = {error: true, message: "an error occurred"};
err.prototype = new Error();
return window.P.reject(err);
}

function StartTheProgram() {
return RejectWithAnError()

// Comment out this catch handler completely, and the Promise stack trace will correctly show the "RejectWithAnError" method as the error origin.
.catch(function (status) {
console.log("[WARN] Catch handler was called.");
// Neither of these lines will show "RejectWithAnError" in the Promise chain stack trace.
// throw status;
return window.P.reject(status);

});
}

StartTheProgram()


(On a side note, this is my first Stack Overflow question, so I hope this is the right format for this question.)

Edit: Updated example to reject using an object instance that inherits from a new
Error
instance.

Answer

Errors in JavaScript capture their trace when they are created so whenever you rethrow the error it will automatically capture the stack trace.

You are however not throwing errors. For this reason bluebird is giving you a warning (yay us). If you insist on throwing non-errors rather than properly subclassing errors - you'll need to manually trick the object to have a proper stack trace by manually capturing it. Either by creating a new Error inside the constructor and setting the .stack to its stack (might have to do some parsing) or by calling a specific method:

function RejectWithAnError() {
    var err = {error: true, message: "an error occurred"};
    // err.__proto__ = new Error(); -- this is how you'd do it with prototypes btw
    // explicitly capture the stack trace of the object
    if(Error.captureStackTrace) Error.captureStackTrace(err);
}

Fiddle here. Just a note, .prototype is a property used by functions to indicate the prototype of objects created by calling the function as a constructor. In order to set the prototype of an object directly you'd call __proto__ although that's rarely a particularly good idea. Here's an example with __proto__ instead of Error.captureStackTrace.