【问题标题】:Passing an async function as a callback causes the error stack trace to be lost将异步函数作为回调传递会导致错误堆栈跟踪丢失
【发布时间】:2019-10-31 19:40:07
【问题描述】:

我正在尝试编写一个函数,该函数将在抛出对象字面量时重新引入堆栈跟踪。 (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();

(Code Sandbox)

输出:在堆栈跟踪中记下我的 cmets

[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

在我看来,等待声明是搞砸了。

这里发生了什么?

【问题讨论】:

  • 我这里还有一些例子:codesandbox.io/s/fast-wave-04fpo
  • 发生这种情况是因为您在 await 表达式之后 throwing 异步函数中这两个 ases 中的错误 - 所以基本上是在 promise 回调中。

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


【解决方案1】:

缺少堆栈跟踪与 Promises 无关。编写具有以同步方式相互调用的函数的相同代码,您将观察到完全相同的行为,即在重新抛出 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 文档涵盖详细信息。 NodeJS 从版本 12.x 开始 will incorporate 更清晰的堆栈跟踪,可通过 V8 引擎提供的 --async-stack-traces 选项获得。

【讨论】:

  • 我必须承认你是绝对正确的,承诺与丢失的上下文无关。我会修改我的答案以反映这一点。
  • 我发布的示例中的第二个堆栈跟踪怎么样?我们可以看到最终导致错误的是 index.js65 是我们熟悉的顶级代码。
  • @dwjohnston 发现我的答案得到了扩展,并解释了堆栈跟踪的缺失元素以及获得更清晰的堆栈跟踪的方法。
  • >编写具有以同步方式相互调用的函数的相同代码,您将观察到完全相同的行为,即在重新抛出新错误时丢失完整的堆栈跟踪数据您能举个例子吗?我觉得我的代码确实这样做了,并且保留了堆栈跟踪。
  • 感谢 v8 参考。这似乎确实触及了问题的核心。
【解决方案2】:

这可能不是一个直接的答案,但我和我的团队正在构建一个库来处理 async/await 承诺,而不需要 try/catch 块。

  1. 安装模块

    npm install await-catcher

  2. 导入 awaitCatcher

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

  3. 使用它!

不要这样做:

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 库很简单。它返回一个有两个索引的数组。

1) 第一个索引包含结果/数据,如果有错误,则为 undefined "[ data , undefined]"

2) 第二个索引包含错误 OR undefined 如果没有错误 "[undefined, error]"


Await-catcher 还支持 TypeScript 中的类型。如果你使用 TypeScript,你可以传递类型来检查返回值。

例子:

 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 引擎在开始新的滴答时“丢失”了错误堆栈跟踪。它仅从该点返回错误堆栈。有人回答了类似的问题here

将您的代码更改为: 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();

结论

最好的做法是抛出一个字符串而不是一个 Error 对象。调试起来会更加困难,并且可能导致丢失错误堆栈跟踪。强烈推荐阅读:Throwing strings instead of Errors

【讨论】:

  • 这很有帮助 - 但我真正要寻找的是堆栈跟踪丢失的技术原因。
  • @dwjohnston 有人回答了类似的问题。 stackoverflow.com/questions/55162619/…我已经更新了我的答案以包含一个解决方案:)
  • @Moekanan 我要解决的问题是我正在使用的库正在抛出对象文字。这就是为什么我要创建一个包装函数。
【解决方案3】:

编辑:这个答案似乎是绝对不正确,请参阅@andy 的回答,它准确地描述了这里发生的事情。

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

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

你能做些什么呢?也许,不能肯定地说,因为从来没有这样做过。但我认为以下是一个好的开始:https://github.com/nodejs/node/issues/11865

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-10-05
    • 2019-11-14
    • 1970-01-01
    • 2018-09-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多