将异步函数作为回调传递会导致错误堆栈跟踪丢失
Passing an async function as a callback causes the error stack trace to be lost
我正在尝试编写一个函数,该函数将在抛出对象文字时重新引入堆栈跟踪。 (See this related question)。
我注意到,如果将一个异步函数作为回调传递给另一个异步调用函数,如果调用函数有一个 try/catch,并捕获任何错误,并抛出一个新错误,然后堆栈跟踪丢失。
我试过几种变体:
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();
输出:在堆栈跟踪中记下我的评论
[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
在我看来,是 await 语句搞砸了。
这是怎么回事?
编辑:这个答案似乎绝对不正确,请参阅@andy 的回答,它准确描述了这里发生的事情。
我认为上下文并没有完全丢失——它从未存在过。您正在使用 async/await,并且您的代码被有效地分成 "chunks",它们以某种 non-linear 的方式执行——异步。这意味着在某些时候解释器离开主线程,执行 'tick'(因此你在堆栈跟踪中看到 process._tickCallback
),然后执行下一个 "chunk".
为什么会这样?因为 async/await 是 Promise
的语法糖,它是由外部事件引导的有效包装回调(我相信在这种特殊情况下它是一个计时器)。
对此你能做些什么?也许,不能肯定地说,因为从来没有这样做过。但我认为以下是一个好的开始:https://github.com/nodejs/node/issues/11865
缺少堆栈跟踪与 Promises 无关。编写具有以同步方式相互调用的函数的相同代码,您将观察到完全相同的行为,即在重新抛出 new Error
时丢失完整的堆栈跟踪数据。只有 Error
对象提供堆栈访问。它反过来由本机代码支持(例如 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 是 JS 引擎 non-standard 扩展。
编辑:关于堆栈上缺少 "everything" 函数,这是引擎如何将 "async/await" 转换为微任务调用以及谁真正调用特定回调的副作用。参考 V8 引擎团队 explanation as well as their zero-cost async stack traces document covering details. NodeJS starting from version 12.x will incorporate 更清晰的堆栈跟踪,可通过 V8 引擎提供的 --async-stack-traces
选项获得。
这可能不是一个直接的答案,但我和我的团队正在构建一个库来处理 async/await 承诺,而不需要 try/catch 块。
安装模块
npm install await-catcher
导入 awaitCatcher
const { awaitCatcher } = require("await-catcher")
用起来!
而不是这样做:
async function asyncFunctionCaller2(fn) {
try {
await fn();
} catch (err) {
throw new Error(err);
}
}
现在您可以这样做了:
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"
await-catcher 库很简单。它 return 是一个有两个索引的数组。
1) 第一个索引包含 results/data OR undefined 如果有错误
"[ data , undefined]"
2) 第二个索引包含错误或未定义如果没有错误
"[undefined, error]"
Await-catcher 也支持 TypeScript 中的类型。如果您使用 TypeScript,您可以传递类型以根据 return 值进行检查。
示例:
interface promiseType {
test: string
}
(async () => {
let p = Promise.resolve({test: "hi mom"})
let [ data , error ] = await awaitCatcher<promiseType>(p);
console.log(data, error);
})()
我们将很快更新我们的 GitHub 存储库以包含文档。
https://github.com/canaanites/await-catcher
编辑:
似乎 V8 引擎是 "losing" 开始新滴答时的错误堆栈跟踪。它只是 return 从那一点开始的错误堆栈。有人回答了类似的问题 .
将您的代码更改为:
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();
结论
抛出字符串而不是错误对象不是最佳做法。调试起来会更加困难,并且可能会导致丢失错误堆栈跟踪。强烈推荐阅读:Throwing strings instead of Errors
我正在尝试编写一个函数,该函数将在抛出对象文字时重新引入堆栈跟踪。 (See this related question)。
我注意到,如果将一个异步函数作为回调传递给另一个异步调用函数,如果调用函数有一个 try/catch,并捕获任何错误,并抛出一个新错误,然后堆栈跟踪丢失。
我试过几种变体:
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();
输出:在堆栈跟踪中记下我的评论
[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
在我看来,是 await 语句搞砸了。
这是怎么回事?
编辑:这个答案似乎绝对不正确,请参阅@andy 的回答,它准确描述了这里发生的事情。
我认为上下文并没有完全丢失——它从未存在过。您正在使用 async/await,并且您的代码被有效地分成 "chunks",它们以某种 non-linear 的方式执行——异步。这意味着在某些时候解释器离开主线程,执行 'tick'(因此你在堆栈跟踪中看到 process._tickCallback
),然后执行下一个 "chunk".
为什么会这样?因为 async/await 是 Promise
的语法糖,它是由外部事件引导的有效包装回调(我相信在这种特殊情况下它是一个计时器)。
对此你能做些什么?也许,不能肯定地说,因为从来没有这样做过。但我认为以下是一个好的开始:https://github.com/nodejs/node/issues/11865
缺少堆栈跟踪与 Promises 无关。编写具有以同步方式相互调用的函数的相同代码,您将观察到完全相同的行为,即在重新抛出 new Error
时丢失完整的堆栈跟踪数据。只有 Error
对象提供堆栈访问。它反过来由本机代码支持(例如 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 是 JS 引擎 non-standard 扩展。
编辑:关于堆栈上缺少 "everything" 函数,这是引擎如何将 "async/await" 转换为微任务调用以及谁真正调用特定回调的副作用。参考 V8 引擎团队 explanation as well as their zero-cost async stack traces document covering details. NodeJS starting from version 12.x will incorporate 更清晰的堆栈跟踪,可通过 V8 引擎提供的 --async-stack-traces
选项获得。
这可能不是一个直接的答案,但我和我的团队正在构建一个库来处理 async/await 承诺,而不需要 try/catch 块。
安装模块
npm install await-catcher
导入 awaitCatcher
const { awaitCatcher } = require("await-catcher")
用起来!
而不是这样做:
async function asyncFunctionCaller2(fn) {
try {
await fn();
} catch (err) {
throw new Error(err);
}
}
现在您可以这样做了:
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"
await-catcher 库很简单。它 return 是一个有两个索引的数组。
1) 第一个索引包含 results/data OR undefined 如果有错误
"[ data , undefined]"
2) 第二个索引包含错误或未定义如果没有错误
"[undefined, error]"
Await-catcher 也支持 TypeScript 中的类型。如果您使用 TypeScript,您可以传递类型以根据 return 值进行检查。
示例:
interface promiseType {
test: string
}
(async () => {
let p = Promise.resolve({test: "hi mom"})
let [ data , error ] = await awaitCatcher<promiseType>(p);
console.log(data, error);
})()
我们将很快更新我们的 GitHub 存储库以包含文档。 https://github.com/canaanites/await-catcher
编辑:
似乎 V8 引擎是 "losing" 开始新滴答时的错误堆栈跟踪。它只是 return 从那一点开始的错误堆栈。有人回答了类似的问题
将您的代码更改为: 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();
结论
抛出字符串而不是错误对象不是最佳做法。调试起来会更加困难,并且可能会导致丢失错误堆栈跟踪。强烈推荐阅读:Throwing strings instead of Errors