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.