Tenemos una base de código bastante compleja en NodeJS que ejecuta muchas promesas de forma sincrónica. Algunos de ellos provienen de Firebase ( firebase-admin
), algunos de otras bibliotecas de Google Cloud, algunos son solicitudes locales de MongoDB. Este código funciona en general bien, se cumplen millones de promesas en el transcurso de 5 a 8 horas.
Pero a veces recibimos promesas rechazadas debido a razones externas como tiempos de espera de la red. Por esta razón, tenemos bloques de prueba y captura en todas las llamadas de Firebase, Google Cloud o MongoDB (las llamadas están en await
, por lo que una promesa rechazada debe ser capturada como los bloques de captura). Si se agota el tiempo de espera de la red, lo intentamos nuevamente después de un tiempo. Esto funciona muy bien la mayor parte del tiempo. A veces, todo funciona sin ningún problema real.
Sin embargo, a veces aún se rechazan promesas no controladas, que luego aparecen en el process.on('unhandledRejection', ...)
. Los rastros de pila de estos rechazos se ven así, por ejemplo:
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)
Este es un seguimiento de pila que está completamente separado de mi propio código, por lo que no tengo ni idea de dónde podría mejorar mi código para hacerlo más robusto contra errores (el mensaje de error también parece ser muy útil).
Otro ejemplo:
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)
Al menos este mensaje de error dice algo.
Todas mis llamadas a Google Cloud o MongoDB tienen bloques de await
e try
: catch
a su alrededor (y la referencia de MongoDB se recrea en el bloque de captura), por lo que si la promesa se rechaza dentro de esas llamadas, el error se detectaría en el bloque de captura.
A veces ocurre un problema similar en la biblioteca de Firebase. Algunas de las promesas rechazadas (por ejemplo, debido a errores de red) quedan atrapadas en nuestros bloques try-catch, pero otras no, y no tengo posibilidad de mejorar mi código, porque no hay seguimiento de pila en ese caso.
Ahora, independientemente de las causas específicas de estos problemas: encuentro muy frustrante que los errores ocurran a escala global ( process.on('unhandledRejection', ...)
, en lugar de en una ubicación en mi código donde puedo manejarlos con un intento de captura Esto nos hace perder mucho tiempo, porque tenemos que reiniciar todo el proceso cuando llegamos a ese estado.
¿Cómo puedo mejorar mi código para que estas excepciones globales no vuelvan a ocurrir? ¿Por qué estos errores son rechazos globales no controlados cuando tengo bloques de intento y captura alrededor de todas las promesas?
Puede darse el caso de que estos sean los problemas de los clientes de MongoDB/Firebase: sin embargo, más de una biblioteca se ve afectada por este comportamiento, por lo que no estoy seguro.
un stacktrace que está completamente separado de mi propio código
Sí, pero ¿la función a la que llama tiene un manejo de errores adecuado para lo que hace TI ?
A continuación, muestro un ejemplo simple de por qué su código externo con try/catch simplemente no puede evitar los rechazos de promesas.
//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 stackoverflow console doesn't show the error }) } async function yourCode(){ try{console.log(await someFireBaseThing())} catch(e){console.warn("successful handle:",e)} } yourCode()
Al leer su pregunta una vez más, parece que puede establecer un límite de tiempo para una tarea y luego throw
manualmente a su catch
de espera si lleva demasiado tiempo (porque si la pila de errores no incluye su código, la promesa que recibe mostrado a unhandledRejection
probablemente no sería visto por su código en primer lugar)
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)} } }
Desarrollando mi comentario, esto es lo que apuesto a que está sucediendo: configura una instancia base de ordenación para interactuar con la API, luego usa esa instancia para avanzar en sus llamadas. Es probable que esa instancia base sea un emisor de eventos que en sí mismo puede emitir un evento de 'error', que es un error fatal no controlado sin una configuración de escucha de 'error'.
Usaré postgres como ejemplo, ya que no estoy familiarizado con firebase o 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 = $1', [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 }
La solución en el ejemplo sería comenzar con
const pool = new (require('pg')).Pool(...); pool.on('error', (err) => { /* do whatever with error */ })