【问题标题】:Why is it possible to try-catch an async-await call?为什么可以尝试捕获异步等待调用?
【发布时间】:2019-02-25 07:18:13
【问题描述】:

在 JavaScript 中有一个常见的反模式:

function handleDataClb(err, data) {
    if(!data) throw new Error('no data found');
    // handle data... 
} 

function f() {
    try {
        fs.readFile('data', 'utf8', handleDataClb);
    } catch(e) {
        // handle error... 
    }
}

f 中的这个 try-catch 不会捕获 handleDataClb 中的错误,因为回调在稍后阶段和上下文中调用,此时 try-catch 不再可见。

现在在 JavaScript 中,异步等待是使用生成器、承诺和协程实现的,如下所示:

// coroutine example 
co(function* doTask() {
    try {
        const res1 = yield asyncTask1(); // returns promise
        const res2 = yield asyncTask2(); // returns promise
        return res1 + res2;
    } catch(e) {
        // handle error... 
    }
});

// async-await example
async function doTask() {
    try {
        const res1 = await asyncTask1(); // returns promise
        const res2 = await asyncTask2(); // returns promise
        return res1 + res2;
    } catch(e) {
        // handle error... 
    }
}

try-catch 以这种方式工作,这通常被认为是 async-await 优于回调的一大优势。

catch 为什么以及如何工作?当asyncTask 调用之一导致promise 拒绝时,协程又名async 如何设法在try-catch 中抛出错误?

编辑:正如其他人所指出的,JavaScript 引擎实现 await 运算符的方式可能与 Babel 等转译器使用的纯 JavaScript 实现非常不同,上面显示为 coroutine example。因此更具体地说:使用本机 JavaScript 是如何工作的?

【问题讨论】:

  • 在高层次上,这就是语言如何协调被拒绝的承诺,因为没有更好的方法来处理它们。使用它们作为返回值只会让事情变得奇怪。如果您想阅读规范,这里有一些不错的地方。 tc39.github.io/ecma262/#await-rejected 。和tc39.github.io/ecma262/#sec-throwcompletion
  • 感谢您的链接!据我了解,JS 引擎当然可以做各种魔术来实现这样的规范。但是纯 JS 协程实现也能达到同样的效果,应该可以用 JS 语法来解释吧?

标签: javascript ecmascript-6 error-handling async-await coroutine


【解决方案1】:

async函数

异步函数返回一个 Promise,该 Promise 由函数体返回的值解析,或被函数体中抛出的错误拒绝。

如果等待的承诺被拒绝,await 运算符返回已履行承诺的值或抛出错误,使用拒绝原因。

await 抛出的错误可以被 async 函数内的 try-catch 块捕获,而不是允许它们向上传播执行堆栈并拒绝通过调用 async 函数返回的承诺。

await 操作符还在返回事件循环之前存储执行上下文,以允许 Promise 操作继续进行。当内部通知等待的承诺已解决时,它会在继续之前恢复执行上下文。

async 函数的执行上下文中设置的try/catch 块不会仅仅因为上下文已被await 保存和恢复而更改或变为无效。

顺便说一句

“async-await 是使用生成器、promise 和协程实现的”

可能是 Babel 如何转换 async 函数和 await 运算符用法的一部分,但可以更直接地实现本机实现。


生成器函数(更新)

生成器函数的执行上下文存储在其关联的生成器对象的内部 [[Generator Context]] 槽中。 (ECMA 2015 25.3.2)

Yield 表达式从执行上下文堆栈的顶部删除生成器的执行上下文 (25.3.3.5 of ES6/ECMAScript 2015)

恢复生成器函数会从生成器对象的 [[Generator Context]] 槽恢复函数的执行上下文。

因此,当yield 表达式返回时,生成器函数可以有效地恢复之前的执行上下文。

由于正常原因(语法错误、throw 语句、调用抛出的函数)在生成器函数中引发错误,可以按预期被 try-catch 块捕获。

通过Generator.prototype.throw() 引发错误会在生成器函数中引发错误,该错误源自最后从生成器函数传递控制权的yield expression。与普通错误一样,try-catch 可以捕获此错误。 (参考 MDN using throw(), ECMA 2015 25.3.3.4

总结

await 转译代码中使用的 yield 语句周围的 Try-catch 块的工作原因与它们在本机 async 函数中围绕 await 运算符所做的工作相同 - 它们在与错误相同的执行上下文中定义为被拒绝的承诺而抛出。

【讨论】:

  • 感谢您的回答!我对A try/catch block ... is not ... rendered ineffective ... because the context has been saved 的方式特别感兴趣。像 Babel 这样的转译器是如何实现的?
  • 我也想知道更多。有没有关于这方面的好文章的链接,而且不会太笨拙?
【解决方案2】:

catch 为什么以及如何工作?协程又名异步如何设法在 try-catch 中抛出错误?

yieldawait 表达式可以有 3 种不同的结果:

  • 它可以像普通表达式一样计算结果值
  • 它可以像throw 语句一样评估,导致异常
  • 它可以像return 语句一样求值,导致在结束函数之前只求值finally 语句

在挂起的生成器上,这可以通过调用.next().throw().return() 方法来实现。 (当然还有第四个可能的结果,永远不会恢复)。

...当其中一个 asyncTask 调用导致 promise 被拒绝时?

awaited 的值将是 Promise.resolve()d 到一个承诺,然后 .then() method 通过两个回调对其进行调用:当承诺履行时,协程以正常值恢复(承诺结果) ,当 promise 被拒绝时,协程会以突然完成的方式恢复(异常 - 拒绝原因)。

您可以查看 co 库代码或转译器输出 - 字面意思是 calls gen.throw from the promise rejection callback

【讨论】:

  • 感谢您的回答 - 因此错误可以在 try-catch 上下文中引发,因为生成器保留上下文并允许使用 throw 恢复它,这正是协程在产生的承诺被拒绝?
  • @BM 是的,完全正确。您可以查看 co 库代码或转译器输出 - 它实际上从承诺拒绝回调中调用 generator.throw
  • 您能否将这句话添加到您的答案中以使其超级容易理解?谢谢!
猜你喜欢
  • 2014-12-01
  • 1970-01-01
  • 2019-10-20
  • 2020-08-06
  • 2018-04-20
  • 2019-10-24
  • 2017-05-17
  • 1970-01-01
  • 2019-10-10
相关资源
最近更新 更多