为什么这些承诺拒绝是全球性的?

Why are these promise rejections global?

我们在 NodeJS 中有一个相当复杂的代码库,它同步运行很多 Promises。其中一些来自 Firebase (firebase-admin),一些来自其他 Google 云库,一些是本地 MongoDB 请求。此代码大部分工作正常,在 5-8 小时内完成了数百万个承诺。

但有时我们会因为网络超时等外部原因而拒绝承诺。出于这个原因,我们在所有 Firebase 或 Google Cloud 或 MongoDB 调用周围都有 try-catch 块(这些调用是 awaited,因此应该捕获被拒绝的承诺块)。如果发生网络超时,我们只是过一会儿再试。这在大多数时候都很好用。有时,整个过程没有任何实际问题。

然而,有时我们仍然会遇到未处理的承诺被拒绝,然后出现在 process.on('unhandledRejection', ...) 中。这些拒绝的堆栈跟踪如下所示,例如:

Warn: Unhandled Rejection at: Promise [object Promise] reason: Error stack: Error: 
    at new ApiError ([repo-path]\node_modules\@google-cloud\common\build\src\util.js:59:15)
    at Util.parseHttpRespBody ([repo-path]\node_modules\@google-cloud\common\build\src\util.js:194:38)
    at Util.handleResp ([repo-path]\node_modules\@google-cloud\common\build\src\util.js:135:117)
    at [repo-path]\node_modules\@google-cloud\common\build\src\util.js:434:22
    at onResponse ([repo-path]\node_modules\retry-request\index.js:214:7)
    at [repo-path]\node_modules\teeny-request\src\index.ts:325:11
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)

这是一个完全独立于我自己的代码的堆栈跟踪,所以我完全不知道我可以在哪里改进我的代码以使其更健壮地抵抗错误(错误消息似乎也很有帮助)。

另一个例子:

Warn: Unhandled Rejection at: Promise [object Promise] reason: MongoError: server instance pool was destroyed stack: MongoError: server instance pool was destroyed
    at basicWriteValidations ([repo-path]\node_modules\mongodb\lib\core\topologies\server.js:574:41)
    at Server.insert ([repo-path]\node_modules\mongodb\lib\core\topologies\server.js:688:16)
    at Server.insert ([repo-path]\node_modules\mongodb\lib\topologies\topology_base.js:301:25)
    at OrderedBulkOperation.finalOptionsHandler ([repo-path]\node_modules\mongodb\lib\bulk\common.js:1210:25)
    at executeCommands ([repo-path]\node_modules\mongodb\lib\bulk\common.js:527:17)
    at executeLegacyOperation ([repo-path]\node_modules\mongodb\lib\utils.js:390:24)
    at OrderedBulkOperation.execute ([repo-path]\node_modules\mongodb\lib\bulk\common.js:1146:12)
    at BulkWriteOperation.execute ([repo-path]\node_modules\mongodb\lib\operations\bulk_write.js:67:10)
    at InsertManyOperation.execute ([repo-path]\node_modules\mongodb\lib\operations\insert_many.js:41:24)
    at executeOperation ([repo-path]\node_modules\mongodb\lib\operations\execute_operation.js:77:17)

至少这条错误消息说明了一些事情。

我所有的 Google Cloud 或 MongoDB 呼叫周围都有 awaittry-catch 块(以及 MongoDB 参考在 catch 块中重新创建),因此如果在这些调用中拒绝了 promise,错误将在 catch 块中被捕获。

Firebase 库中有时会发生类似的问题。一些被拒绝的承诺(例如由于网络错误)被我们的 try-catch 块捕获,但有些没有,我没有可能改进我的代码,因为在那种情况下没有堆栈跟踪。

现在,不管这些问题的具体原因是什么:我发现错误只是在全球范围内发生(process.on('unhandledRejection', ...),而不是在我的代码中可以处理它们的位置,这让我非常沮丧with a try-catch。这让我们浪费了很多时间,因为当我们进入这种状态时,我们必须重新启动整个过程。

如何改进我的代码,使这些全局异常不再发生?为什么当我在所有 promise 周围设置 try-catch 块时,这些错误是全局未处理的拒绝?

这些可能是 MongoDB / Firebase 客户端的问题:但是,不止一个库受到此行为的影响,所以我不确定。

a stacktrace which is completely detached from my own code

是的,但是您调用的函数是否对 IT 的功能进行了适当的错误处理?
下面我展示了一个简单的例子,说明为什么带有 try/catch 的外部代码可以简单地 not 防止承诺拒绝

//if a function you don't control causes an error with the language itself, yikes

//and for rejections, the same(amount of YIKES) can happen if an asynchronous function you call doesn't send up its rejection properly
//the example below is if the function is returning a custom promise that faces a problem, then does `throw err` instead of `reject(err)`)

//however, there usually is some thiAPI.on('error',callback) but try/catch doesn't solve everything
async function someFireBaseThing(){
  //a promise is always returned from an async function(on error it does the equivalent of `Promise.reject(error)`)
  //yet if you return a promise, THAT would be the promise returned and catch will only catch a `Promise.reject(theError)`
  
  return await new Promise((r,j)=>{
    fetch('x').then(r).catch(e=>{throw e})
    //unhandled rejection occurs even though e gets thrown
    //ironically, this could be simply solved with `.catch(j)`
    //check inspect element console since Whosebug console doesn't show the error
  })
}
async function yourCode(){
  try{console.log(await someFireBaseThing())}
  catch(e){console.warn("successful handle:",e)}
}
yourCode()

再次阅读您的问题后,您似乎可以为任务设置时间限制,然后手动 throw 等待 catch 如果时间太长(因为如果错误堆栈不包含您的代码,显示给 unhandledRejection 的承诺可能首先不会被您的代码看到)

function handler(promise,time){ //automatically rejects if it takes too long
  return new Promise(async(r,j)=>{
    try{let temp=await promise; r(temp)} catch(err){j(err)}
    setTimeout(()=>j('promise did not resolve in given time'),time)
  })
}
async function yourCode(){
  while(true){ //will break when promise is successful(and returns)
    try{return await handler(someFireBaseThing(...someArguments),1e4)}
    catch(err){yourHandlingOn(err)}
  }
}

详细说明我的评论,我敢打赌这是怎么回事:你设置了一些排序基础实例来与 API 交互,然后在你的调用中使用该实例。该基础实例可能是一个事件发射器,它本身可以发射一个 'error' 事件,这是一个致命的未处理错误,没有 'error' 侦听器设置。

我将使用 postgres 作为示例,因为我不熟悉 firebase 或 mongo。


// Pool is a pool of connections to the DB
const pool = new (require('pg')).Pool(...);

// Using pool we call an async function in a try catch
try {
  await pool.query('select foo from bar where id = ', [92]);
}
catch(err) {
  // A SQL error like no table named bar would be caught here.
  // However a connection error would be emitted as an 'error'
  // event from pool itself, which would be unhandled
}

示例中的解决方案将从

开始
const pool = new (require('pg')).Pool(...);
pool.on('error', (err) => { /* do whatever with error */ })