为什么可以尝试捕获异步等待调用?

Why is it possible to try-catch an async-await call?

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 async-await 是使用生成器、promises 和协程实现的,如:

// 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 和如上所示 coroutine example。因此,更具体地说:使用本机 JavaScript 是如何工作的?

async 函数

异步函数 returns 由函数主体返回的值解析的承诺,或由主体中抛出的错误拒绝的承诺。

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

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

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

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

顺便说一句

"async-await is implemented using generators, promises, and coroutines"

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


生成器函数(更新)

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

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

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

因此,当 yield 表达式 returns.

时,生成器函数有效地恢复了先前的执行上下文

由于正常原因(语法错误、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 运算符的原因相同 - 它们是在与为拒绝的承诺抛出错误相同的执行上下文中定义。

Why and how does the catch work? How does the coroutine aka async manage to throw the error inside the try-catch?

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

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

在挂起的发电机上,这可以通过调用 .next(), .throw() or .return() 方法之一来实现。 (当然还有第4种可能的结果,永不恢复)。

…when one of the asyncTask calls results in a promise rejection?

awaited 值将 Promise.resolve()d 传递给承诺,然后 .then() method 通过两个回调在其上调用:当承诺实现时,协同程序恢复正常值(promise 结果),当 promise 被拒绝时,协同程序会突然完成(异常 - 拒绝原因)恢复。

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