【问题标题】:Under what circumstances would a finally block not be reached?在什么情况下不会到达 finally 块?
【发布时间】:2021-06-08 21:32:22
【问题描述】:

除了通常的嫌疑人(process.exit(),或进程终止/信号,或崩溃/硬件故障)之外,是否存在无法到达 finally 块中的代码的情况?

以下打字稿代码通常按预期执行(使用 node.js),但偶尔会在第 4 行立即终止,不会引发异常或更改进程退出代码(退出 0/成功):

1 import si from 'systeminformation';
2 async populateResolvedValue() {
3   try {
4     const osInfo = await si.osInfo();
5     ...
6   } finally {
7     console.log('whew!');  //  <=========== NOT REACHED!
8   }
9 }

我已经在 IJ 调试会话中验证了这一点 - 第 7 行上的 finally 块偶尔不会执行,并将在第 4 行立即终止(堆栈展开一些)。我知道这可能发生在哪里(并且仍然成功退出)的唯一情况是,如果在 someAsyncFunc() 中的某个地方遇到了段错误,但我添加了“段错误处理程序”,但没有出现任何内容。

我也尝试过使用 Promise.then/finally 而不是 async/await 与 try/finally 语义 - 完全相同的行为。

node.js:v12.18.2 和 v14.16.0

【问题讨论】:

  • si.osInfo() 是否正在返回承诺?
  • 也许si.osInfo 会停止等待任何可以使事件循环保持活动状态的异步事件,但无法解决让您的代码继续运行的承诺。对我来说,这听起来像是那个库中的一个错误。
  • 如果 promise 没有解决,await 操作符不会一直等到它解决或遇到超时吗?为什么它会突然退出该函数堆栈框架?
  • await 运算符不会“等待”帧仍在调用堆栈上,它每次都会暂停执行并清除调用堆栈。承诺解决后,它将恢复该功能(就像then 处理程序一样)——这在您的情况下永远不会发生。如果没有正在运行的异步任务,promise 上的处理程序不会阻止 nodejs 退出事件循环。
  • @Dilshan 事实上,here 中的classic Promise constructor antipattern

标签: javascript node.js typescript


【解决方案1】:

感谢所有 cmets,我能够解决核心问题并找到满意的答案。

简而言之,是的 - 除了通常的嫌疑人之外,如果所讨论的承诺永远不会“解决”,则可能永远不会到达并执行 Promise.finally(或 try/finally with await)块。一些承诺可能需要比预期更长的时间来解决(I/O 等待等)并最终解决或拒绝,但如果一个承诺从不解决(通过错误或设计)所有代码都取决于该承诺(thenfinally 等)永远不会执行。

那么什么是未确定的承诺,它们是如何发生的?如果从不调用其 resolvereject 回调函数(由于错误或设计),则 Promise 可能永远不会解决。

考虑以下承诺:

 1 iPromiseTo(): Promise<any> {
 2   return new Promise((resolve, reject) => {
 3     if (this.willNeverHappen()) {
 4       resolve('I can forp');                    <====== Resolved
 5     } else if (this.willAlsoNeverHappen()) {
 6       reject('I cannot forp but I can rarp');   <====== Rejected
 7     } else {
 8       console.log('I cannot promise anything'); <====== Unsettled
 9     }
10   });
11 }

如果 promise 函数体在没有解析/拒绝的情况下完成(通过第 8 行到第 10 行)任何依赖代码,包括稍后附加的 then/catch/finally 块(见下文)将根本不执行,并将被静默忽略。在大多数情况下,这可能被认为是一个错误,这就是为什么async 函数在大多数情况下都是有利的。 async 函数在其函数体完成时将始终解析或拒绝(其函数体可能永远不会完成,但这是另一个主题)。

Promise 语义中使用这个有缺陷的承诺:

 1
 2 youllBeSorry() {
 3   this.iPromiseTo()
 4     .then(promised => {
 5       console.log(promised); //  <=========== NOT REACHED!
 6     }).catch(error => {
 7       console.log(error);    //  <=========== NOT REACHED!
 8     }).finally(() =>
 9       console.log('whew!');  //  <=========== NOT REACHED!
10     });
11 }

第 5、7 和 9 行不会执行。

在 async/await 语义中使用它:

 1
 2 async youllBeSorry() {
 3   try {
 4     const promised = await this.iPromiseTo();
 5     ...                    //  <=========== NOT REACHED!
 6   } catch(error) {
 7     console.log(error);    //  <=========== NOT REACHED!
 8   } finally {
 9     console.log('whew!');  //  <=========== NOT REACHED!
10   }
11 }

第 5、7 和 9 行将不会执行,函数将在第 4 行静默返回,不会出现错误或任何后续异常处理。

另一类可能永远不会解决的 promise 是那些由于无限循环或无限挂起等(由于错误或设计)而永远不会完成的函数体。它们实际上的行为类似于那些完成其函数体但从不调用resolvereject 的promise,尽管这两者之间可能存在其他差异,此处未涵盖。

好的,那么我该如何处理这些“未解决”的承诺呢?不幸的是,没有规定的方法来捕捉和处理这种特殊的承诺结果(在撰写本文时)。有一些建议,但我自己没有尝试过: await/async how to handle unresolved promises

您可能已经可以使用顶级 await,或者即将推出(另请参阅其较旧的 IIFE 解决方法): https://v8.dev/features/top-level-await

理想情况下,ECMAScript 将对异步行为提供更多顶级控制 - 请参阅 kotlin(甚至是 python)协程,了解如何更好地执行异步的更强大的示例。

最后(没有双关语),我会注意到 finally通常 在这种特殊情况下按预期执行的原因是正在使用 oclif 框架,不幸的是它有 @ 987654338@ 调用嵌入在其 API 例程中。在所有情况下,所讨论的承诺都可以实现/解决,但它与 oclif 中的退出调用竞争,因此并不总是得到解决,并且依赖代码并不总是在抛出退出异常和节点退出之前运行。所以原来是这个特殊案例中的“常见嫌疑人”之一,但故障排除导致发现了 ECMAScript 中的这些缺点。

【讨论】:

    【解决方案2】:

    除了通常的嫌疑人(process.exit(),或进程 终止/信号,或崩溃/硬件故障),是否有任何 无法到达 finally 块中的代码的情况?

    如果承诺将来会解决或拒绝,那么它应该到达最后一个块。


    根据 MDN 文档,

    finally-block 包含要在try-block 之后执行的语句 和catch-block(s) 执行,但在后面的语句之前 try...catch...finally-块。注意finally-block 执行 不管是否抛出异常。此外,如果异常是 抛出,finally-block 中的语句即使没有也执行 catch-block 处理异常。

    promise 只是一个 JavaScript 对象。一个对象可以有很多状态。 Promise 对象可以处于pending 状态或settled 状态。状态settled 可以分为fulfilledrejected。对于这个例子,假设我们只有两个状态 PENDINGSETTLED

    现在,如果承诺永远不会解决或拒绝,那么它永远不会进入settled 状态,这意味着您的then..catch..finally 永远不会调用。如果没有任何东西是对 Promise 的引用,那么它只会被垃圾回收。


    在您最初的问题中,您提到了第 3 方异步 method。如果您看到该代码,您首先可以看到的是,有一组if(..) 块来确定当前的操作系统。 但它没有任何else 块或默认情况。

    如果没有if(..) 块被触发怎么办?没有什么可以执行,你已经用return new Promise()返回了一个承诺。所以基本上如果没有if(..) 块被触发,promise 将永远不会将其状态从pending 更改为settled

    然后正如 @Bergi 也提到有一些这样的代码。正如他所提到的,一个经典的 Promise 构造函数反模式。例如看下面的代码,

      isUefiLinux().then(uefi => {
        result.uefi = uefi;
        uuid().then(data => {
          result.serial = data.os;
          if (callback) {
            callback(result);
          }
          resolve(result);
        });
      });
    

    如果上述isUefiLinux 从未解决怎么办?同样then 不会在isUefiLinux 上触发并且永远不会解决主要承诺。

    现在,如果您检查 isUefiLinux 的代码,即使它抛出错误,它也正在解析。

    function isUefiLinux() {
      return new Promise((resolve) => {
        process.nextTick(() => {
          fs.stat('/sys/firmware/efi', function (err) {
            //what if this cb never called?
            if (!err) {
              resolve(true);
            } else {
              exec('dmesg | grep -E "EFI v"', function (error, stdout) {
                //what if this cb never called?
                if (!error) {
                  const lines = stdout.toString().split('\n');
                  resolve(lines.length > 0);
                }
                resolve(false);
              });
            }
          });
        });
      });
    }
    

    但是isUefiLinux 方法中有两个回调函数,一个是promise 和回调的混合体;一个“地狱”。现在,如果这些回调从未被调用怎么办?你的承诺永远不会兑现。


    我已经在 IJ 调试会话中验证了这一点 - finally 块 第 7 行偶尔不会执行,并会立即终止 第 4 行

    “偶尔”不会执行?上面的解释不是在某种程度上解释了这一点吗?

    更多信息

    1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
    2. https://tc39.es/ecma262/#sec-promise.prototype.finally
    3. https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md

    【讨论】:

    • "如果上面的 isUefiLinux 从未解决怎么办?" - 这不是问题 - 或者,不是调用者的问题;预计最终会解决一个承诺,就像预计在某个时候调用异步回调一样。真正的问题是isUefiLinux() 拒绝了承诺,它没有在任何地方处理而是被忽略,不会导致osInfo 承诺得到解决。
    • @Bergi,是的,一般来说,代码的那个区域可能会出现问题,但在这种特殊情况下不是问题,因为我在 mac (darwin) 上运行。
    • @Bergi 这就是我的意思。 isUefiLinux 不处理异常。因此,从技术上讲,在该特定代码中接下来唯一可以运行的是then 块。如果这也没有调用(这意味着没有通过调用 resolve 或由于异常或任何原因来解决)那么会发生什么?这就是我被问到的。
    猜你喜欢
    • 2012-09-07
    • 2017-06-27
    • 1970-01-01
    • 2013-03-14
    • 1970-01-01
    • 1970-01-01
    • 2010-10-18
    • 2012-04-23
    • 2011-10-16
    相关资源
    最近更新 更多