从异步捕获中抛出时 Stacktrace 不完整

Stacktrace incomplete when throwing from async catch

为什么重新抛出异步异常时没有异步堆栈跟踪?对于节点 12+,异常 运行 the following code:

async function crash() {
    try {
        await (async () => {throw new Error('dead');})();
    } catch (e) {
        throw new Error('rethrow');
    }
}

async function foo() {
    await new Promise(resolve => setTimeout(() => resolve(), 1));
    await crash();
}

async function entrypoint() {
    try {
        await foo();
    } catch(e) {
        console.log(e.stack);
    }
}
entrypoint();

严重不完整:

Error: rethrow
    at crash (/async-stackt/crash.js:6:15)

我找到了一个变通方法 by defining the exception in the beginning of crash(),它的效果更好:

Error: rethrow
    at crash (/workaround.js:2:17)
    at foo (/workaround.js:12:11)
    at async entrypoint (/workaround.js:17:9)

这不是最优的,因为无论是否需要错误都必须提前构造,并且堆栈跟踪有些不精确。

为什么从异步捕获块中抛出错误时堆栈跟踪不完整?是否有任何解决方法或更改代码以首先获得完整的堆栈跟踪?

该堆栈跟踪显示了当时堆栈中的内容。当您进行诸如 setTimeout() 之类的异步调用时,它 运行 是 setTimeout() 注册一个计时器,该计时器将在未来某个时间触发,然后继续执行。由于您在此处使用 await,它会暂停 foo() 的执行,但会继续执行调用 foo() 之后的代码。因为那也是一个 await,它继续执行调用 entrypoint() 的代码。完成后,堆栈完全为空。

然后,稍后,您的计时器会触发,并且会使用完全干净的堆栈调用其回调。在您的情况下, setTimeout() 回调仅调用 resolve() ,然后触发承诺在下一个事件循环滴答时将其解析处理程序安排到 运行 。 returns 回到系统,栈帧再次为空。在事件循环的下一个滴答声中,promise 解析处理程序被调用,并且满足函数上下文中该 promise 上的 await。当 await 满足时,该函数的其余部分开始执行。

当该函数执行结束时,解释器知道这是一个挂起的函数上下文。函数中没有 return 发生,因为这已经发生了。相反,因为这是一个 async 函数,函数执行的结束解决了这个 async 函数 returns 的承诺。解决该承诺然后安排其解决处理程序在事件循环的下一个滴答时调用,然后它 return 将控制权交还给系统。堆栈帧再次为空。在事件循环的下一个滴答声中,它调用满足 await foo() 语句中的 await 的解析处理程序,并且 entrypoint() 函数可以继续 运行,从哪里开始上次暂停。

所以,这里的关键是当计时器关闭时,执行从 foo 回到 entrypoint,而不是通过堆栈和 return 语句(该函数已经return 不久前编辑),但通过承诺得到解决。因此,当计时器关闭然后调用 crash() 时,堆栈确实是空的,除了对 crash() 本身的函数调用。

当 promise 被 resolve 时,这个空栈的概念是 async 函数实际工作原理的核心,所以理解这一点很重要。您必须记住,它会暂停包含 await 的函数的内部执行,但是一旦它命中第一个 await,它就会立即导致函数 return 一个 promise 和调用者继续进一步执行。调用者不会暂停,除非他们也如此 await 在这种情况下调用者的调用者继续执行。在某个时候,有人开始继续执行,最终,它 return 将控制权交还给系统,此时堆栈已为空。

定时器事件(或其他一些 promise 触发事件)随后被调用,并带有一个完全空的堆栈帧,没有来自原始调用序列的残余。

不幸的是,我现在知道的唯一解决方法是执行您发现的操作 - 在原始堆栈仍然存在时更早地创建 Error 对象。如果我没记错的话,有一个关于向 Javascript 语言添加一些特性以使异步跟踪更容易的讨论。我不记得提案的细节,但也许通过记住最初调用函数时堆栈帧是什么,因为它在承诺之后是 resolved/rejected 并且创建错误对象时不再非常有用。


如果有人不熟悉 async 函数的工作原理以及它们如何在第一个 await 时暂停自己的执行,但后来 return 过早,这里有一个小演示:

function delay(t) {
    return new Promise(resolve => {
        setTimeout(resolve, t);
    });
}

async function stepA() {
    console.log("5");
    await stepB();
    console.log("6");
}

async function stepB() {
    console.log("3");
    await delay(50);
    console.log("4");
}

console.log("1");
stepA();
console.log("2");

这会生成以下输出。如果您逐步遵循此执行路径,您将看到每个 await 如何从该函数导致早期 return,然后可以看到堆栈帧将如何在 promise 被执行后为空等待得到解决。这是生成的输出:

1
5
3
2
4
6

很清楚为什么 1 是第一个,因为它是第一个要执行的东西。

那么,为什么先调用stepA(),然后5就很清楚了。

然后,stepA 调用 stepB() 以便它开始执行,这就是我们接下来看到 3 的原因。

然后,stepB 调用 await delay(50)。这会执行 delay(50) 启动一个计时器,然后立即 return 一个与该计时器挂钩的承诺。然后它命中 await 并停止执行 stepB.

stepB 命中那个 await 时,它导致 stepB 在那个点上 return 来自函数 async 的承诺。该承诺将最终(在将来)与 stepB 执行挂钩,从而有机会完成其所有执行。现在 stepB 的执行被暂停。

stepB return 兑现承诺时,它会回到 stepA 执行 await stepB(); 的地方。现在 stepB() 已经 returned(未实现的承诺),然后 stepAunfulfilled 承诺上实现了 await。这会暂停 stepA 的执行,并且 return 在这一点上是一个承诺。

所以,现在对 stepA() 的原始函数调用已经 returned(一个未实现的承诺)并且在该函数调用上没有 await,之后的顶级代码该函数调用继续执行,我们看到控制台输出 2.

console.log("2") 是最后要在这里执行的语句,因此控制权 return 返回给解释器。此时,栈帧完全为空

然后,一段时间后,计时器启动。这会在 JS 事件队列中插入一个事件。当 JS 解释器空闲时,它会拾取该事件并调用与该事件关联的计时器回调。这只做一件事(调用 resolve() 承诺)然后 returns。在该承诺计划上调用 resolve 承诺在事件循环的下一个滴答时触发它的 .then() 处理程序。发生这种情况时,代码行 await delay(50); 上的 await 得到满足,该函数的执行将恢复。然后我们在控制台中看到 4 作为 stepB 的最后一行执行。

console.log("4"); 执行后,stepB 现在已经完成执行,它可以解析它的 async 承诺(之前由它 return 编辑的承诺)。解决该承诺会告诉它为事件循环的下一个滴答安排其 .then() 处理程序。控制返回到 JS 解释器。

在事件循环的下一个滴答声中,.then() 处理程序通知 await stepB(); 中的 await 承诺现已解决并执行 stepA继续,现在我们在控制台中看到 6。这是要执行的 stepA 的最后一行,它可以解析其 async 承诺并将 return 控制权交还给系统。

事实证明,没有人听 async 承诺调用 stepA() return,因此没有进一步执行。

我 运行 在 Node 12 中遇到了同样的问题,发现它在更高版本的 V8 中得到了修复:commit.

我猜要等待节点 14...

该错误与未在 catch 块中跟踪的堆栈跟踪有关。因此,一种解决方法是改用 Promise 的 .catch 函数:

async function crash() {
    await (async () => {throw new Error('dead');})()
        .catch(e => { throw new Error('rethrow'); });
}