Manejo de errores simples para aplicaciones de Node
El manejo de errores simples en Node puede ser muy difícil porque el almacenamiento local de continuación (CLS) es difícil y extraño, por lo que no se habla bien de él.
CLS es una variable asociada con un hilo de ejecución particular (semántico), que lo hace a través de la devolución de llamada. La gente lo hace.
Solo se necesitan estas bibliotecas increíblemente invasivas e inteligentes que envuelven literalmente todo lo asincrónico en un contenedor.
Y si tiene cosas como un grupo de conexiones, es muy fácil estropearlo (porque un objeto creado en un contexto se reutilizará en otro). En esta publicación, nuestro objetivo es escribir un código que todos puedan entender y no vamos a hablar sobre el manejo avanzado de errores simples de CLS.
Deberíamos hacer más async/await y no hacer callback hell
No queremos hacer CLS completo. En última instancia, creo que es demasiado complejo para que valga la pena, pero otros equipos podrían tomar una decisión diferente.
- CLS es mucho más necesario en el infierno de devolución de llamada y MENOS necesario con async/await
- Hay una pila mantenida por el intérprete que es literalmente una lista de contextos.
Lanzar una excepción y atraparla en la parte superior de la persona que llama.
Digamos que tiene 4 funciones asíncronas fn1, fn2, fn3, fn4 y fn1 llama a fn2, fn2 llama a fn3 y así sucesivamente.
Si lanza fn4 y lo atrapa en fn4, no sabría en la pila de errores quién es la persona que llama porque no puede atrapar lo que no se le lanza.
- Si está buscando un marco de pila que se parece a
fn4:x fn3:y fn2:z fn1:o
(donde x, y, z, o son números de línea),- entonces lanza a fn4 y lo atrapa en fn1.
// BAD async function fn1 (req, res) { // does bunch of stuff here ... // I died, I can't tell you why :( const resultFn2 = await fn2(d) } async function fn2 (d) { // does bunch of stuff here ... const resultFn3 = await fn3(d) } async function fn3 (e) { // does bunch of stuff here with e ... const resultFn4 = await fn4(f) } async function fn4 (f) { // does bunch here with f return Promise.reject(new Error(`HooHaa! Kill Ya!`)) .catch(function (e) { logger.error(e) }); } // GOOD async function fn1 (req, res) { try { // does bunch of stuff here ... const resultFn2 = await fn2(d) } catch (e) { const cause = e.message logger.error(new Error('Failed at fn1 cause', { cause })) } } async function fn2 (d) { // does bunch of stuff here ... const resultFn3 = await fn3(e) } async function fn3 (e) { // does bunch of stuff here with e ... const resultFn4 = await fn4(f) } async function fn4 (f) { // does bunch here with f throw new Error(`HooHaa! Kill Ya!`) }
Pero quiero que fn4 sepa sobre `request` obj que fn1 está obteniendo y capturando en fn4 y registrando allí.
Erm, eso suena como una violación de límites para que un código más profundo se preocupe por la persona que llama.
He escuchado tales requisitos y sugeriría propagar el error hasta la persona que llama donde se puede acceder al objeto de request
.
// BAD async function fn1 (req, res) { const res = await fn2(req); } async function fn2 (req) { try { await foobar(); } catch (e) { // sent req to this function because we wanted to log it } } // GOOD async function fn1 (req, res) { try { const res = await fn2(); } catch (e) { // you have access to req here, you can log more info } } async function fn2 () { return foobar(); }
Usar error.causa
Desde Node v16 en adelante, obtiene la propiedad de cause
en el objeto Error
. Doc dice:
“Si está presente, la propiedad error.cause es la causa subyacente del error. Se utiliza cuando se detecta un error y se lanza uno nuevo con un mensaje o código diferente para poder seguir teniendo acceso al error original”
Si está bloqueado en versiones anteriores de Node, puede usar https://www.npmjs.com/package/verror
const cause = new Error('The remote HTTP server responded with a 500 status');
const symptom = new Error('The message failed to send', { cause });
console.log(symptom);
// Prints:
// Error: The message failed to send
// at REPL2:1:17
// at Script.runInThisContext (node:vm:130:12)
// ... 7 lines matching cause stack trace ...
// at [_line] [as _line] (node:internal/readline/interface:886:18) {
// [cause]: Error: The remote HTTP server responded with a 500 status
// at REPL1:1:15
// at Script.runInThisContext (node:vm:130:12)
// at REPLServer.defaultEval (node:repl:574:29)
// at bound (node:domain:426:15)
// at REPLServer.runBound [as eval] (node:domain:437:12)
// at REPLServer.onLine (node:repl:902:10)
// at REPLServer.emit (node:events:549:35)
// at REPLServer.emit (node:domain:482:12)
// at [_onLine] [as _onLine] (node:internal/readline/interface:425:12)
// at [_line] [as _line] (node:internal/readline/interface:886:18)
Entonces, básicamente, si uso async/await, ¿no hay ningún problema con el manejo de errores en el nodo?
Siempre hay problemas que resolver. Pero no, es mucho más claro en un mundo asíncrono/en espera. Todavía hay problemas en los límites en torno a cosas como grupos de conexiones:
- si almacena promesas pendientes, las pilas pueden volverse inestables, pero no está mal.
- Especialmente los errores de envoltura, esas pilas desordenadas más profundas estarían principalmente en los errores de causa internos.
Por ejemplo: si tiene un grupo de conexiones, está lleno, por lo que se inicia una nueva conexión y una solicitud no usa la conexión (tal vez debido a un error, o al usarlo innecesariamente, o algo así), se devuelve al piscina, a menudo como una promesa pendiente.
- Esa conexión falla, y luego otra solicitud obtiene esa conexión del grupo y obtiene esa excepción…
- con la pila de la primera solicitud. No es un gran problema, pero los grupos de conexiones tienen un manejo de errores complejo.
¿Cuándo dejo caer el error?
Cuando el error ha sido manejado. O cuando no hay ninguna acción que se pueda tomar sobre el error, y se espera que suceda.
- Por lo general, puede manejar un error en dos lugares:
- cerca de la fuente, donde sabe lo que está sucediendo y puede intentar otra cosa o volver a intentarlo; y de muy alto nivel,
- donde sabe lo que se pretendía y cuál es el contexto, y señala su falla o vuelve a intentar la tarea de alto nivel.
- Son las capas intermedias las que generalmente solo deberían pasar errores. Rara vez los manejas allí.
Gracias por llegar hasta aquí, si encuentras esto útil no olvides dejar un👍🏼y suscribirse para recibir más contenido.
Si le interesa, puede echar un vistazo a algunos de los otros artículos que he escrito recientemente sobre AWS y Laravel: