How to chain exceptions in javascript (ie add caus

2020-03-01 02:45发布

问题:

Is there a standard / best practice way to add a cause of an exception in javascript. In java you might do this:

Throwable t = new Exception("whatever");
t.addCause(previouslyCaughtException);
throw t;

When the resulting exception is printed, it'll give you a nice trace that includes causes. Is there any good way to do this in javascript or do I have to roll my own?

回答1:

In prod, we use TraceError

Usage

import TraceError from 'trace-error';

global.TraceError = TraceError; // expose globally (optional)

throw new TraceError('Could not set status', srcError, ...otherErrors);

Output



回答2:

For now (until there's a better answer), this is what I've done:

...
} catch(e) {
  throw new Error("My error message, caused by: "+e.stack+"\n ------The above causes:-----")
}

The way I'm printing exceptions makes it nice and clean looking:

console.log(e.stack)

prints something like this:

My error message: SomeError
<some line>
<more lines>
------The above causes:-----
<some line>
<more lines>

The line might be better if it said "causes" because the stack trace of the exception causing the error is printed first.



回答3:

If you are using Node.js you can use VError

Joyent also have a page discussing best practices for error handling in Node.js https://www.joyent.com/developers/node/design/errors

The only thing is I don't like how Joyent's implementation of VError falls over if you pass in a null or undefined parameters. This is particularly important when you are handling errors as it'll just mask the root cause of the problem. I've forked their VError so it will not fail with null or undefined parameters. https://github.com/naddison36/node-verror



回答4:

tl;dr This is not a solution, just a helper till ECMA Script adopts some standard.

EDIT: I wrapped this answer into the chainable-error npm package.

Well this is a kind of a difficult topic. The reason is, there is no definition about the stack trace in the ECMA Script definition (not even in ES9 / ES2019)). So some engines implement their own idea of an stack trace and its representation.

Many of them have implemented the Error.prototype.stack property which is a string representation of the stack trace. Since this is not defined you can not rely on the string format. Luckily the V8 engine is quite common (Google Chrome and NodeJS) which gives us a chance to at least try to.

A good thing about the V8 (and the applications using it) is that the Stack trace has a common format:

/path/to/file/script.js:11
        throw new Error("Some new Message", e);
        ^

Error: Some new Message
    at testOtherFnc (/path/to/file/script.js:69:15)
    at Object.<anonymous> (/path/to/file/script.js:73:1)
    at Module._compile (internal/modules/cjs/loader.js:688:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10)
    at Module.load (internal/modules/cjs/loader.js:598:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:537:12)
    at Function.Module._load (internal/modules/cjs/loader.js:529:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:741:12)
    at startup (internal/bootstrap/node.js:285:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3)

...and the stack trace is not parsed and styled in the console.

Which gives us a good opportunity to chain them (or at least change the output generated of the error).

A quite easy way to do this would be something like this:

let ff = v => JSON.stringify(v, undefined, 4);
const formatForOutput = v => {
    try {
        return ff(v).replace(/\n/g, '\n    ');
    } catch (e) {
        return "" + v;
    }
};

const chainErrors = exporting.chainErrors = (e1, e2) => {
    if (e1 instanceof Error)
        e2.stack += '\nCaused by: ' + e1.stack;
    else
        e2.stack += '\nWas caused by throwing:\n    ' + formatForOutput(e1);

    return e2;
}

Which you could use like this:

function someErrorThrowingFunction() {
    throw new Error("Some Message");
}

function testOtherFnc() {
    try {
        someErrorThrowingFunction();
    } catch (e) {
        throw chainErrors(e, new Error("Some new Message"));
    }
}

Which produces:

/path/to/file/script.js:11
        throw new Error("Some new Message", e);
        ^

Error: Some new Message
    at testOtherFnc (/path/to/file/script.js:11:15)
    at Object.<anonymous> (/path/to/file/script.js:15:1)
    at Module._compile (internal/modules/cjs/loader.js:688:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10)
    at Module.load (internal/modules/cjs/loader.js:598:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:537:12)
    at Function.Module._load (internal/modules/cjs/loader.js:529:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:741:12)
    at startup (internal/bootstrap/node.js:285:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3)
Caused by: Error: Some Message
    at someErrorThrowingFunction (/path/to/file/script.js:4:11)
    at testOtherFnc (/path/to/file/script.js:9:9)
    at Object.<anonymous> (/path/to/file/script.js:15:1)
    at Module._compile (internal/modules/cjs/loader.js:688:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10)
    at Module.load (internal/modules/cjs/loader.js:598:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:537:12)
    at Function.Module._load (internal/modules/cjs/loader.js:529:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:741:12)
    at startup (internal/bootstrap/node.js:285:19)

Which is pretty similar to the stack trace generated by Java. There are three problems with this.

The first problem is the duplication of the call sites, which is solveable but complicated.

The second is that the output generated is engine dependent, this attempt works quite well for V8 but is not usable for Firefox for example, since Firefox not just uses another style but they also parse and style the error massage which prevents us from chaining it like this.

The third problem is usability. This is a little bit clunky, you have to remember this function and you need to keep track if you are in the right engine. Another way to do this would be something like this:

const Error = (() => {
    const glob = (() => { try { return window; } catch (e) { return global; } })();

    const isErrorExtensible = (() => {
        try {
            // making sure this is an js engine which creates "extensible" error stacks (i.e. not firefox)
            const stack = (new glob.Error('Test String')).stack;
            return stack.slice(0, 26) == 'Error: Test String\n    at ';
        } catch (e) { return false; }
    })();

    const OriginalError = glob.Error;

    if (isErrorExtensible) {
        let ff = v => JSON.stringify(v, undefined, 4);
        const formatForOutput = v => {
            try {
                return ff(v).replace(/\n/g, '\n    ');
            } catch (e) {
                return "" + v;
            }
        };

        const chainErrors = (e1, e2) => {
            if (e1 instanceof OriginalError)
                e2.stack += '\nCaused by: ' + e1.stack;
            else
                e2.stack += '\nWas caused by throwing:\n    ' + formatForOutput(e1);

            return e2;
        }

        class Error extends OriginalError {
            constructor(msg, chained) {
                super(msg);

                if (arguments.length > 1)
                    chainErrors(chained, this);
            }
        }

        return Error;
    } else
        return OriginalError; // returning the original if we can't chain it
})();

And then you could do it just like in Java:

function someErrorThrowingFunction() {
    throw new Error("Some Message");
}

function testOtherFnc() {
    try {
        someErrorThrowingFunction();
    } catch (e) {
        throw new Error("Some new Message", e);
    }
}

testOtherFnc();

Even though the second version brings some (other) problems with it might be the "easier" one, since you do not need to change your code even when the engine does not support the chaining because you could give a function (the Error constructor) as many parameters as you want.

Either way, hopefully this will be something for ES2020.



回答5:

You can chain errors objects Error doing a concat of stack and message.

var console = {
    log: function(s) {
      document.getElementById("console").innerHTML += s + "<br/>"
    }
  }

var error1=new Error("This is error 1");
console.log("Message: ".concat( error1.message ));
console.log("Stack<br>".concat(error1.stack) );

var error2=new Error("This is error 2");
console.log("Message: ".concat( error2.message) );
console.log("Stack<br>".concat( error2.stack) );

var error3=new Error("This is error 3");
error3.stack=error3.stack.concat(error2.stack).concat(error1.stack)
console.log("Message: ".concat(error3.message));
console.log("Stack<br>".concat(error3.stack));
<div id="console" />