传递异步函数作为回调会导致错误堆栈跟踪丢失

时间:2019-06-18 08:00:45

标签: javascript error-handling async-await try-catch stack-trace

我正在尝试编写一个函数,该函数将在引发对象文字时重新引入堆栈跟踪。 (See this related question)。

我注意到的是,如果将异步函数作为回调传递给另一个异步调用者函数,则如果调用者函数具有try / catch并捕获任何错误,并引发新的Error,则堆栈跟踪迷路。

我已经尝试了几种方法:

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();

Code Sandbox

输出:在堆栈跟踪中记录我的评论

[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

在我看来,wait语句正在使问题变得更糟。

这是怎么回事?

3 个答案:

答案 0 :(得分:6)

缺少堆栈跟踪与Promise无关。编写相同的代码,它们具有以同步方式相互调用的函数,您将观察到完全相同的行为,即,在重新抛出new Error时失去了完整的堆栈跟踪数据。只有Error个对象提供堆栈访问。反过来,它由负责捕获交叉堆栈帧的堆栈跟踪的本机代码(如this of V8 engine)支持。每次创建Error对象时,情况都会变得更糟,它会从此时开始跨堆栈帧捕获堆栈(至少在浏览器中是可见的,nodejs的实现可能有所不同)。这样,如果您捕获并回溯了不同的Error对象,则在冒泡异常的顶部可以看到其堆栈跟踪。缺少Error的异常链(无法将新的异常包裹在捕获的异常周围)使得很难填补这些空白。更有趣的是,ECMA-262 spec chapter 19.5根本没有引入Error.prototype.stack属性,在MDN中,您反过来发现stack property是JS引擎非标准扩展。

编辑:关于堆栈上缺少“所有”功能,这是引擎如何将“异步/等待”转换为微任务调用以及谁真正在调用特定的回调的副作用。请参考V8引擎团队explanation以及他们的zero-cost async stack traces文档以了解详细信息。从版本12.x will incorporate开始的NodeJS更清晰的堆栈跟踪,可通过V8引擎提供的--async-stack-traces选项获得。

答案 1 :(得分:0)

这可能不是直接的答案,但是我和我的团队正在建立一个库来处理异步/等待诺言,而无需try / catch块。

  1. 安装模块

    [[lettuce, tomatoes, ginger, vodka, tomatoes] [lettuce, tomatoes, flour, vodka, tomatoes] ... [flour, tomatoes, vodka, vodka, mustard] [[0, 1, 2, 3, 1] [0, 1, 4, 3, 1] ... [4, 1, 3, 3, 9]]

  2. 导入awaitCatcher

    npm install await-catcher

  3. 使用它!

代替这样做:

const { awaitCatcher } = require("await-catcher")

现在您可以执行以下操作:

async function asyncFunctionCaller2(fn) {
  try {
    await fn();
  } catch (err) {
    throw new Error(err);
  }
}

await-catcher库很简单。它返回一个带有两个索引的数组。

1)第一个索引包含结果/数据,如果有错误,则未定义 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"

2)第二个索引包含错误;如果没有错误,则未定义 "[ data , undefined]"


Await-catcher还支持TypeScript中的类型。如果使用TypeScript,则可以传递要与返回值对照的类型。

示例:

"[undefined, error]"

我们将很快更新GitHub存储库以包含文档。 https://github.com/canaanites/await-catcher


编辑:

似乎V8引擎在开始新的滴答声时正在“丢失”错误堆栈跟踪。它仅从该点返回错误堆栈。有人回答了类似的问题here

将代码更改为此: https://codesandbox.io/embed/empty-wave-k3tdj

 interface promiseType {
     test: string
 }

 (async () => {
     let p = Promise.resolve({test: "hi mom"})
     let [ data , error ] = await awaitCatcher<promiseType>(p);
     console.log(data, error);
 })()

结论

最好不是抛出Error对象而是抛出字符串。这将更加难以调试,并可能导致丢失错误堆栈跟踪。强烈建议您阅读以下内容:Throwing strings instead of Errors

答案 2 :(得分:-1)

编辑:此答案似乎完全不正确,请参阅@andy的答案,其中确切描述了这里发生的情况。

我认为上下文并没有完全丢失-它从未出现过。您正在使用async / await,并且您的代码有效地分成了“块”,这些块以某种非线性的方式执行-异步执行。这意味着解释器在某些时候会离开主线程,进行“滴答”(因此您在堆栈跟踪中看到process._tickCallback)并执行下一个“块”。

为什么会这样?因为async / await是Promise的语法糖,所以它可以有效地包装由外部事件引导的回调(我相信在这种情况下,它是一个计时器)。

您能对此做什么?也许不能肯定地说从来没有那样。但我认为以下是一个好的开始:https://github.com/nodejs/node/issues/11865