Reputation: 11842
I'm trying to write a function that will reintroduce a stack trace when an object literal is thrown. (See this related question).
What I've noticed is that if a pass an async function as a callback into another async caller function, if the caller function has a try/catch, and catches any errors, and throws a new Error, then the stack trace gets lost.
I've tried several variants of this:
function alpha() {
throw Error("I am an error!");
}
function alphaObectLiberal() {
throw "I am an object literal!"; //Ordinarily this will cause the stack trace to be lost.
}
function syncFunctionCaller(fn) {
return fn();
}
function syncFunctionCaller2(fn) { //This wrapper wraps it in a proper error and subsequently preserves the stack trace.
try {
return fn();
} catch (err) {
throw new Error(err); //Stack trace is preserved when it is synchronous.
}
}
async function asyncAlpha() {
throw Error("I am also an error!"); //Stack trace is preseved if a proper error is thown from callback
}
async function asyncAlphaObjectLiteral() {
throw "I am an object literal!"; //I want to catch this, and convert it to a proper Error object.
}
async function asyncFunctionCaller(fn) {
return await fn();
}
async function asyncFunctionCaller2(fn) {
try {
await fn();
} catch (err) {
throw new Error(err);
}
}
async function asyncFunctionCaller3(fn) {
try {
await fn();
} catch (err) {
throw new Error("I'm an error thrown from the function caller!");
}
}
async function asyncFunctionCaller4(fn) {
throw new Error("No try catch here!");
}
async function everything() {
try {
syncFunctionCaller(alpha);
} catch (err) {
console.log(err);
}
try {
syncFunctionCaller2(alphaObectLiberal);
} catch (err) {
console.log(err);
}
try {
await asyncFunctionCaller(asyncAlpha);
} catch (err) {
console.log(err);
}
try {
await asyncFunctionCaller2(asyncAlphaObjectLiteral);
} catch (err) {
console.log(err); //We've lost the `everthing` line number from the stack trace
}
try {
await asyncFunctionCaller3(asyncAlphaObjectLiteral);
} catch (err) {
console.log(err); //We've lost the `everthing` line number from the stack trace
}
try {
await asyncFunctionCaller4(asyncAlphaObjectLiteral);
} catch (err) {
console.log(err); //This one is fine
}
}
everything();
Output: note my comments in the stack trace
[nodemon] starting `node src/index.js localhost 8080`
Error: I am an error!
at alpha (/sandbox/src/index.js:2:9)
at syncFunctionCaller (/sandbox/src/index.js:6:10)
at everything (/sandbox/src/index.js:43:5)
//We can see what function caused this error
at Object.<anonymous> (/sandbox/src/index.js:73:1)
at Module._compile (internal/modules/cjs/loader.js:776:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
at Module.load (internal/modules/cjs/loader.js:653:32)
at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
at Function.Module._load (internal/modules/cjs/loader.js:585:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
Error: I am an object literal!
at syncFunctionCaller2 (/sandbox/src/index.js:17:11)
at everything (/sandbox/src/index.js:65:5)
//In a synchronous wrapper, the stack trace is preserved
at Object.<anonymous> (/sandbox/src/index.js:95:1)
at Module._compile (internal/modules/cjs/loader.js:776:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
at Module.load (internal/modules/cjs/loader.js:653:32)
at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
at Function.Module._load (internal/modules/cjs/loader.js:585:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
at startup (internal/bootstrap/node.js:283:19)
Error: I am also an error!
at asyncAlpha (/sandbox/src/index.js:10:9)
at asyncFunctionCaller (/sandbox/src/index.js:18:16)
at everything (/sandbox/src/index.js:49:11)
//We can see what function caused this error
at Object.<anonymous> (/sandbox/src/index.js:73:1)
at Module._compile (internal/modules/cjs/loader.js:776:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
at Module.load (internal/modules/cjs/loader.js:653:32)
at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
at Function.Module._load (internal/modules/cjs/loader.js:585:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
Error: I am an object literal!
at asyncFunctionCaller2 (/sandbox/src/index.js:25:11)
//We've lost the stacktrace in `everything`
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:832:11)
at startup (internal/bootstrap/node.js:283:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
Error: I'm an error thrown from the function caller!
at asyncFunctionCaller3 (/sandbox/src/index.js:33:11)
//We've lost the stacktrace in `everything`
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:832:11)
at startup (internal/bootstrap/node.js:283:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
Error: No try catch here!
at asyncFunctionCaller4 (/sandbox/src/index.js:38:9)
at everything (/sandbox/src/index.js:67:11)
//We can see what function caused this error
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:832:11)
at startup (internal/bootstrap/node.js:283:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
[nodemon] clean exit - waiting for changes before restart
It seems to me that the await statement is what is screwing this up.
What's going on here?
Upvotes: 17
Views: 10702
Reputation: 189
This might not be a direct answer, but my team and I are building a library to handle async/await promises without the need for try/catch blocks.
Install the module
npm install await-catcher
Import the awaitCatcher
const { awaitCatcher } = require("await-catcher")
Use it!
Instead of doing this:
async function asyncFunctionCaller2(fn) {
try {
await fn();
} catch (err) {
throw new Error(err);
}
}
Now you can do this:
async function asyncFunctionCaller2(fn) {
let [ data, err ] = await awaitCatcher(fn);
// Now you can do whatever you want with data or error
if ( err ) throw err;
if ( data ) return data;
}
// Note:
// You can name the variables whatever you want.
// They don't have to be "data" or "err"
The await-catcher library is simple. It returns an array with two indexes.
1) The first index contains the results/data OR undefined if there is an error
"[ data , undefined]"
2) The second index contains the error OR undefined if there is no error
"[undefined, error]"
Await-catcher also supports types in TypeScript. You can pass types to be checked against the return value if you use TypeScript.
Example:
interface promiseType {
test: string
}
(async () => {
let p = Promise.resolve({test: "hi mom"})
let [ data , error ] = await awaitCatcher<promiseType>(p);
console.log(data, error);
})()
We will update our GitHub repo to include documentation very soon. https://github.com/canaanites/await-catcher
EDIT:
Seems like the V8 engine is "losing" the error stack trace when it starts a new tick. It only returns the error stack from that point. Someone has answered a similar question here.
Change your code to this: https://codesandbox.io/embed/empty-wave-k3tdj
const { awaitCatcher } = require("await-catcher");
async function asyncAlphaObjectLiteral() {
throw Error("I am an object literal!"); // 1) You need to create an Error object here
// ~~~~> try throwing just a string and see the difference
}
async function asyncFunctionCaller2(fn) {
try {
await fn();
} catch (err) {
throw err; // 2) Don't create a new error, just throw the error.
}
}
/**
* Or you can just do this...
* the "awaitCatcher" will catch the errors :)
*
* async function asyncFunctionCaller2(fn) {
* await fn();
* }
*/
async function everything() {
/**
* notice we don't need try/catch here either!
*/
let [data, error] = await awaitCatcher(
asyncFunctionCaller2(asyncAlphaObjectLiteral)
);
console.log(error); // 3) Now you have the full error stack trace
}
everything();
Conclusion
It is not a best practice to throw a string instead of an Error object. It will be more difficult to debug and might cause to lose the error stack trace. Highly recommend reading this: Throwing strings instead of Errors
Upvotes: 0
Reputation: 787
Missing stack trace has nothing to do with Promises. Write the same code that has functions calling each other in synchronous way and you will observe exactly the same behavior i.e. loosing complete stack trace data when rethrowing new Error
. It is only Error
object that offers stack access. It in turn is backed by native code (like this of V8 engine) responsible for capturing stack trace of crossed stack frames. To make it worse each time you create Error
object it captures stack from this point across the stackframes (at least it is observable in browser, nodejs implementation may differ). So that if you catch and retrow different Error
object then its stack trace is visible on top of bubbling exception. Missing exceptions chaining for Error
(no way to wrap new exception around caught one) makes it hard to fill these gaps. More interesting is that ECMA-262 spec chapter 19.5 does not introduce Error.prototype.stack
property at all, in MDN in turn you find stack property is JS engine non-standard extension.
EDIT: Regarding missing "everything" function on stack it is side effect of how engine translates "async/await" into microtask calls and who is really calling specific callbacks. Refer to V8 engine team explanation as well as their zero-cost async stack traces document covering details. NodeJS starting from version 12.x will incorporate more cleaner stack traces, available with --async-stack-traces
option offered by V8 engine.
Upvotes: 8
Reputation: 2357
EDIT: this answer seems to be absolutely incorrect, see answer by @andy which describes exactly what is going on here.
I think the context is not exactly lost -- it was never there. You're using async/await, and your code is effectively split into "chunks" which are executed in somewhat non-linear way -- asynchronously. Which means that at certain points interpreter leaves main thread, does a 'tick' (thus you see process._tickCallback
in stacktrace), and executes next "chunk".
Why that happens? Because async/await is a syntactic sugar to Promise
, which is effectively nicely wrapped callbacks guided by external events (I believe in this particular case it is a timer).
What can you do about this? Maybe, can't say for sure as never did that. But I think the following is a good start: https://github.com/nodejs/node/issues/11865
Upvotes: -1