Saltar al contenido

Manejo de excepciones de mejores prácticas de Node.js

Después de de una prolongada compilación de información hemos podido solucionar esta contrariedad que tienen ciertos usuarios. Te ofrecemos la solución y esperamos servirte de mucha apoyo.

Solución:

Actualización: Joyent ahora tiene su propia guía. La siguiente información es más un resumen:

Errores de “lanzamiento” de forma segura

Idealmente, nos gustaría evitar errores no detectados tanto como sea posible, como tal, en lugar de lanzar literalmente el error, podemos “lanzar” el error de manera segura usando uno de los siguientes métodos dependiendo de nuestra arquitectura de código:

  • Para el código síncrono, si ocurre un error, devuelva el error:

    // Define divider as a syncrhonous function
    var divideSync = function(x,y) 
        // if error condition?
        if ( y === 0 ) 
            // "throw" the error safely by returning it
            return new Error("Can't divide by zero")
        
        else 
            // no error occured, continue on
            return x/y
        
    
    
    // Divide 4/2
    var result = divideSync(4,2)
    // did an error occur?
    if ( result instanceof Error ) 
        // handle the error safely
        console.log('4/2=err', result)
    
    else 
        // no error occured, continue on
        console.log('4/2='+result)
    
    
    // Divide 4/0
    result = divideSync(4,0)
    // did an error occur?
    if ( result instanceof Error ) 
        // handle the error safely
        console.log('4/0=err', result)
    
    else 
        // no error occured, continue on
        console.log('4/0='+result)
    
    
  • Para el código basado en devolución de llamada (es decir, asíncrono), el primer argumento de la devolución de llamada es err, si ocurre un error err es el error, si no ocurre un error, entonces err es null. Cualquier otro argumento sigue el err argumento:

    var divide = function(x,y,next) 
        // if error condition?
        if ( y === 0 ) 
            // "throw" the error safely by calling the completion callback
            // with the first argument being the error
            next(new Error("Can't divide by zero"))
        
        else 
            // no error occured, continue on
            next(null, x/y)
        
    
    
    divide(4,2,function(err,result)
        // did an error occur?
        if ( err ) 
            // handle the error safely
            console.log('4/2=err', err)
        
        else 
            // no error occured, continue on
            console.log('4/2='+result)
        
    )
    
    divide(4,0,function(err,result)
        // did an error occur?
        if ( err ) 
            // handle the error safely
            console.log('4/0=err', err)
        
        else 
            // no error occured, continue on
            console.log('4/0='+result)
        
    )
    
  • Para código accidentado, donde el error puede ocurrir en cualquier lugar, en lugar de lanzar el error, active el error evento en su lugar:

    // Definite our Divider Event Emitter
    var events = require('events')
    var Divider = function()
        events.EventEmitter.call(this)
    
    require('util').inherits(Divider, events.EventEmitter)
    
    // Add the divide function
    Divider.prototype.divide = function(x,y)
        // if error condition?
        if ( y === 0 ) 
            // "throw" the error safely by emitting it
            var err = new Error("Can't divide by zero")
            this.emit('error', err)
        
        else 
            // no error occured, continue on
            this.emit('divided', x, y, x/y)
        
    
        // Chain
        return this;
    
    
    // Create our divider and listen for errors
    var divider = new Divider()
    divider.on('error', function(err)
        // handle the error safely
        console.log(err)
    )
    divider.on('divided', function(x,y,result)
        console.log(x+'/'+y+'='+result)
    )
    
    // Divide
    divider.divide(4,2).divide(4,0)
    

“Detectar” errores de forma segura

A veces, sin embargo, todavía puede haber código que arroja un error en algún lugar que puede conducir a una excepción no detectada y un posible bloqueo de nuestra aplicación si no lo detectamos de manera segura. Dependiendo de nuestra arquitectura de código, podemos usar uno de los siguientes métodos para capturarlo:

  • Cuando sabemos dónde está ocurriendo el error, podemos envolver esa sección en un dominio node.js

    var d = require('domain').create()
    d.on('error', function(err)
        // handle the error safely
        console.log(err)
    )
    
    // catch the uncaught errors in this asynchronous or synchronous code block
    d.run(function()
        // the asynchronous or synchronous code that we want to catch thrown errors on
        var err = new Error('example')
        throw err
    )
    
  • Si sabemos dónde está ocurriendo el error es el código síncrono, y por alguna razón no podemos usar dominios (quizás la versión anterior del nodo), podemos usar la declaración try catch:

    // catch the uncaught errors in this synchronous code block
    // try catch statements only work on synchronous code
    try 
        // the synchronous code that we want to catch thrown errors on
        var err = new Error('example')
        throw err
     catch (err) 
        // handle the error safely
        console.log(err)
    
    

    Sin embargo, tenga cuidado de no utilizar try...catch en código asincrónico, ya que no se detectará un error lanzado de forma asincrónica:

    try 
        setTimeout(function()
            var err = new Error('example')
            throw err
        , 1000)
    
    catch (err) 
        // Example error won't be caught here... crashing our app
        // hence the need for domains
    
    

    Si quieres trabajar con try..catch junto con el código asincrónico, al ejecutar Node 7.4 o superior, puede usar async/await de forma nativa para escribir sus funciones asincrónicas.

    Otra cosa con la que tener cuidado try...catch es el riesgo de envolver su devolución de llamada de finalización dentro del try declaración así:

    var divide = function(x,y,next) 
        // if error condition?
        if ( y === 0 ) 
            // "throw" the error safely by calling the completion callback
            // with the first argument being the error
            next(new Error("Can't divide by zero"))
        
        else 
            // no error occured, continue on
            next(null, x/y)
        
    
    
    var continueElsewhere = function(err, result)
            throw new Error('elsewhere has failed')
    
    
    try 
            divide(4, 2, continueElsewhere)
            // ^ the execution of divide, and the execution of 
            //   continueElsewhere will be inside the try statement
    
    catch (err) 
            console.log(err.stack)
            // ^ will output the "unexpected" result of: elsewhere has failed
    
    

    Este problema es muy fácil de hacer a medida que su código se vuelve más complejo. Como tal, es mejor usar dominios o devolver errores para evitar (1) excepciones no detectadas en código asincrónico (2) el intento de capturar la ejecución de captura que no desea. En los lenguajes que permiten un subproceso adecuado en lugar del estilo de máquina de eventos asincrónica de JavaScript, esto es un problema menor.

  • Finalmente, en el caso de que ocurra un error no detectado en un lugar que no estaba incluido en un dominio o una declaración try catch, podemos hacer que nuestra aplicación no se bloquee usando el uncaughtException oyente (sin embargo, hacerlo puede poner la aplicación en un estado desconocido):

    // catch the uncaught errors that weren't wrapped in a domain or try catch statement
    // do not use this in modules, but only in applications, as otherwise we could have multiple of these bound
    process.on('uncaughtException', function(err) 
        // handle the error safely
        console.log(err)
    )
    
    // the asynchronous or synchronous code that emits the otherwise uncaught error
    var err = new Error('example')
    throw err
    

A continuación se incluye un resumen y una selección de muchas fuentes diferentes sobre este tema, incluidos ejemplos de código y citas de publicaciones de blog seleccionadas. La lista completa de mejores prácticas se puede encontrar aquí.


Mejores prácticas de manejo de errores de Node.JS


Número1: use promesas para el manejo de errores asíncronos

TL; DR: Manejar los errores asíncronos en el estilo de devolución de llamada es probablemente el camino más rápido al infierno (también conocido como la pirámide de la fatalidad). El mejor regalo que puede darle a su código es utilizar en su lugar una biblioteca de promesas de buena reputación que proporcione una sintaxis de código muy compacta y familiar, como try-catch

De lo contrario: El estilo de devolución de llamada de Node.JS, función (err, respuesta), es una forma prometedora de código no mantenible debido a la combinación de manejo de errores con código casual, anidación excesiva y patrones de codificación incómodos

Ejemplo de código: bueno

doWork()
.then(doWork)
.then(doError)
.then(doWork)
.catch(errorHandler)
.then(verify);

ejemplo de código anti patrón – manejo de errores de estilo de devolución de llamada

getData(someParameter, function(err, result)
    if(err != null)
      //do something like calling the given callback function and pass the error
    getMoreData(a, function(err, result)
          if(err != null)
            //do something like calling the given callback function and pass the error
        getMoreData(b, function(c) 
                getMoreData(d, function(e) 
                    ...
                );
            );
        );
    );
});

Cita del blog: “Tenemos un problema con las promesas”
(Del blog pouchdb, clasificado 11 para las palabras clave “Promesas de nodo”)

“… Y, de hecho, las devoluciones de llamada hacen algo aún más siniestro: nos privan de la pila, que es algo que generalmente damos por sentado en los lenguajes de programación. Escribir código sin una pila es muy parecido a conducir un automóvil sin un pedal de freno: no te das cuenta de lo mucho que lo necesitas, hasta que lo alcanzas y no está allí. El objetivo de las promesas es devolvernos los fundamentos del lenguaje que perdimos cuando nos volvimos asíncronos: retorno, lanzamiento y la pila. Pero hay que saber utilizar correctamente las promesas para aprovecharlas.


Número2: use solo el objeto Error incorporado

TL; DR: Es bastante común ver código que arroja errores como una cadena o como un tipo personalizado; esto complica la lógica de manejo de errores y la interoperabilidad entre módulos. Ya sea que rechace una promesa, arroje una excepción o emita un error, el uso del objeto Error incorporado de Node.JS aumenta la uniformidad y evita la pérdida de información de error

De lo contrario: Al ejecutar algún módulo, no estar seguro del tipo de errores que se producen a cambio hace que sea mucho más difícil razonar sobre la próxima excepción y manejarla. Incluso vale la pena, el uso de tipos personalizados para describir errores podría conducir a la pérdida de información crítica de errores como el seguimiento de la pila.

Ejemplo de código: hacerlo bien

    //throwing an Error from typical function, whether sync or async
 if(!productToAdd)
 throw new Error("How can I add new product when no value provided?");

//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));

//'throwing' an Error from a Promise
 return new promise(function (resolve, reject) {
 DAL.getProduct(productToAdd.id).then((existingProduct) =>{
 if(existingProduct != null)
 return reject(new Error("Why fooling us and trying to add an existing product?"));

ejemplo de código anti patrón

//throwing a String lacks any stack trace information and other important properties
if(!productToAdd)
    throw ("How can I add new product when no value provided?");

Cita del blog: “Una cadena no es un error”
(Del blog devthought, clasificado 6 para las palabras clave “Objeto de error de Node.JS”)

“… pasar una cadena en lugar de un error da como resultado una menor interoperabilidad entre los módulos. Rompe los contratos con las API que podrían estar realizando una instancia de comprobaciones de errores o que quieran saber más sobre el error. Los objetos de error, como veremos, tienen propiedades muy interesantes en los motores JavaScript modernos además de contener el mensaje pasado al constructor .. “


Número 3: distinguir errores operativos frente a errores del programador

TL; DR: Los errores de operaciones (por ejemplo, la API recibió una entrada no válida) se refieren a casos conocidos en los que el impacto del error se comprende completamente y se puede manejar con cuidado. Por otro lado, el error del programador (por ejemplo, al intentar leer una variable indefinida) se refiere a fallas de código desconocido que exigen reiniciar correctamente la aplicación.

De lo contrario: Siempre puede reiniciar la aplicación cuando aparezca un error, pero ¿por qué dejar que ~ 5000 usuarios en línea se desconecten debido a un error menor y previsto (error operativo)? lo contrario tampoco es lo ideal: mantener la aplicación activa cuando se produce un problema desconocido (error del programador) puede generar un comportamiento impredecible. Diferenciar los dos permite actuar con tacto y aplicar un enfoque equilibrado en función del contexto dado.

Ejemplo de código: hacerlo bien

    //throwing an Error from typical function, whether sync or async
 if(!productToAdd)
 throw new Error("How can I add new product when no value provided?");

//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));

//'throwing' an Error from a Promise
 return new promise(function (resolve, reject) {
 DAL.getProduct(productToAdd.id).then((existingProduct) =>{
 if(existingProduct != null)
 return reject(new Error("Why fooling us and trying to add an existing product?"));

ejemplo de código: marcar un error como operativo (confiable)

//marking an error object as operational 
var myError = new Error("How can I add new product when no value provided?");
myError.isOperational = true;

//or if you're using some centralized error factory (see other examples at the bullet "Use only the built-in Error object")
function appError(commonType, description, isOperational) 
    Error.call(this);
    Error.captureStackTrace(this);
    this.commonType = commonType;
    this.description = description;
    this.isOperational = isOperational;
;

throw new appError(errorManagement.commonErrors.InvalidInput, "Describe here what happened", true);

//error handling code within middleware
process.on('uncaughtException', function(error) 
    if(!error.isOperational)
        process.exit(1);
);

Cotización del blog: “De lo contrario, arriesga el estado” (Del blog debugable, clasificado 3 para las palabras clave “Excepción no detectada de Node.JS”)

… Por la naturaleza misma de cómo funciona throw en JavaScript, casi nunca hay forma de “retomar donde lo dejaste” de forma segura, sin filtrar referencias o crear algún otro tipo de estado frágil indefinido. La forma más segura de responder a un error arrojado es cerrar el proceso.. Por supuesto, en un servidor web normal, es posible que tenga muchas conexiones abiertas, y no es razonable cerrarlas abruptamente porque alguien más provocó un error. El mejor enfoque es enviar una respuesta de error a la solicitud que desencadenó el error, mientras deja que los demás terminen en su tiempo normal y dejen de escuchar nuevas solicitudes en ese trabajador “


Número 4: gestione los errores de forma centralizada, a través de middleware, pero no dentro de él.

TL; DR: La lógica de manejo de errores, como el correo al administrador y el registro, debe encapsularse en un objeto dedicado y centralizado al que todos los puntos finales (p. Ej., Middleware Express, trabajos cron, pruebas unitarias) llaman cuando aparece un error.

De lo contrario: No manejar los errores en un solo lugar conducirá a la duplicación del código y probablemente a errores que se manejen de manera incorrecta.

Ejemplo de código: un flujo de error típico

//DAL layer, we don't handle errors here
DB.addDocument(newCustomer, (error, result) => 
    if (error)
        throw new Error("Great error explanation comes here", other useful parameters)
);

//API route code, we catch both sync and async errors and forward to the middleware
try 
    customerService.addNew(req.body).then(function (result) 
        res.status(200).json(result);
    ).catch((error) => 
        next(error)
    );

catch (error) 
    next(error);


//Error handling middleware, we delegate the handling to the centrzlied error handler
app.use(function (err, req, res, next) 
    errorHandler.handleError(err).then((isOperationalError) => 
        if (!isOperationalError)
            next(err);
    );
);

Cita del blog: “A veces, los niveles más bajos no pueden hacer nada útil excepto propagar el error a la persona que llama” (Del blog Joyent, clasificado 1 por las palabras clave “Manejo de errores de Node.JS”)

“… Puede terminar manejando el mismo error en varios niveles de la pila. Esto sucede cuando los niveles inferiores no pueden hacer nada útil excepto propagar el error a su llamador, que propaga el error a su llamador, y así sucesivamente. A menudo, solo la persona que llama de nivel superior sabe cuál es la respuesta adecuada, ya sea para volver a intentar la operación, informar un error al usuario o cualquier otra cosa. Pero eso no significa que deba intentar informar todos los errores a un solo nivel superior devolución de llamada, porque esa devolución de llamada en sí misma no puede saber en qué contexto ocurrió el error “


Número 5: errores de la API de documentos al usar Swagger

TL; DR: Informe a las personas que llaman a la API qué errores pueden surgir a cambio para que puedan manejarlos cuidadosamente sin fallar. Esto generalmente se hace con marcos de documentación de API REST como Swagger

De lo contrario: Un cliente de API puede decidir fallar y reiniciar solo porque recibió un error que no pudo entender. Nota: la persona que llama a su API puede ser usted (muy típico en un entorno de microservicios)

Cita del blog: “Tienes que decirles a las personas que llaman qué errores pueden ocurrir” (Del blog Joyent, clasificado 1 por las palabras clave “Registro de Node.JS”)

… Hemos hablado sobre cómo manejar los errores, pero cuando estás escribiendo una nueva función, ¿cómo entregas errores al código que llamó a tu función? … Si no sabe qué errores pueden ocurrir o no sabe lo que significan, entonces su programa no puede ser correcto excepto por accidente. Entonces, si está escribiendo una nueva función, debe decirles a las personas que llaman qué errores pueden ocurrir y qué significan.


Número 6: Cierra el proceso con gracia cuando un extraño llega a la ciudad.

TL; DR: Cuando se produce un error desconocido (un error del desarrollador, consulte la práctica recomendada número 3), existe incertidumbre sobre el estado de la aplicación. Una práctica común sugiere reiniciar el proceso con cuidado utilizando una herramienta de ‘reinicio’ como Forever y PM2

De lo contrario: Cuando se detecta una excepción desconocida, algún objeto puede estar en un estado defectuoso (por ejemplo, un emisor de eventos que se usa globalmente y ya no dispara eventos debido a alguna falla interna) y todas las solicitudes futuras pueden fallar o comportarse de manera loca

Ejemplo de código: decidir si chocar

//deciding whether to crash when an uncaught exception arrives
//Assuming developers mark known operational errors with error.isOperational=true, read best practice #3
process.on('uncaughtException', function(error) 
 errorManagement.handler.handleError(error);
 if(!errorManagement.handler.isTrustedError(error))
 process.exit(1)
);


//centralized error handler encapsulates error-handling related logic 
function errorHandler(){
 this.handleError = function (error) 
 return logger.logError(err).then(sendMailToAdminIfCritical).then(saveInOpsQueueIfCritical).then(determineIfOperationalError);
 

 this.isTrustedError = function(error)
 
 return error.isOperational;
 

Cita del blog: “Hay tres escuelas de pensamiento sobre el manejo de errores” (Del blog jsrecipes)

… Hay principalmente tres escuelas de pensamiento sobre el manejo de errores: 1. Deje que la aplicación se bloquee y reiníciela. 2. Maneje todos los errores posibles y nunca se bloquee. 3. Enfoque equilibrado entre los dos


Número 7: use un registrador maduro para aumentar la visibilidad de errores

TL; DR: Un conjunto de herramientas de registro maduras, como Winston, Bunyan o Log4J, acelerará el descubrimiento y la comprensión de errores. Así que olvídate de console.log.

De lo contrario: Hojear console.logs o manualmente a través de un archivo de texto desordenado sin herramientas de consulta o un visor de registro decente puede mantenerte ocupado en el trabajo hasta tarde

Ejemplo de código: Winston logger en acción

//your centralized logger object
var logger = new winston.Logger(
 level: 'info',
 transports: [
 new (winston.transports.Console)(),
 new (winston.transports.File)( filename: 'somefile.log' )
 ]
 );

//custom code somewhere using the logger
logger.log('info', 'Test Log Message with some parameter %s', 'some parameter',  anything: 'This is metadata' );

Cita del blog: “Identifiquemos algunos requisitos (para un registrador):” (Del blog strongblog)

… Identifiquemos algunos requisitos (para un registrador): 1. Marque la hora de cada línea de registro. Este es bastante autoexplicativo: debería poder saber cuándo ocurrió cada entrada de registro. 2. El formato de registro debe ser fácilmente digerible tanto por humanos como por máquinas. 3. Permite múltiples flujos de destino configurables. Por ejemplo, es posible que esté escribiendo registros de seguimiento en un archivo, pero cuando se encuentre un error, escriba en el mismo archivo, luego en el archivo de error y envíe un correo electrónico al mismo tiempo …


Número 8: Descubra errores y tiempo de inactividad con productos APM

TL; DR: Los productos de supervisión y rendimiento (también conocidos como APM) miden de forma proactiva su base de código o API para que puedan resaltar automáticamente y por arte de magia los errores, los fallos y las piezas lentas que faltaban.

De lo contrario: Es posible que dedique un gran esfuerzo a medir el rendimiento de la API y los tiempos de inactividad, probablemente nunca se dará cuenta de cuáles son sus partes de código más lentas en un escenario del mundo real y cómo afectan a la UX

Cita del blog: “Segmentos de productos APM” (del blog Yoni Goldberg)

“… Los productos APM constituyen 3 segmentos principales:1. Monitoreo de sitios web o API – servicios externos que monitorean constantemente el tiempo de actividad y el rendimiento a través de solicitudes HTTP. Se puede configurar en pocos minutos. A continuación se muestran algunos contendientes seleccionados: Pingdom, Uptime Robot y New Relic
2. Instrumentación de código – familia de productos que requieren integrar un agente dentro de la aplicación para beneficiarse de la función de detección lenta de código, estadísticas de excepciones, monitoreo del rendimiento y muchos más. A continuación se muestran algunos contendientes seleccionados: New Relic, App Dynamics
3. Panel de inteligencia operativa – esta línea de productos se centra en facilitar al equipo de operaciones con métricas y contenido seleccionado que ayuda a mantenerse al tanto del rendimiento de la aplicación. Por lo general, esto implica la agregación de múltiples fuentes de información (registros de aplicaciones, registros de bases de datos, registros de servidores, etc.) y el trabajo de diseño del tablero por adelantado. A continuación se muestran algunos candidatos seleccionados: Datadog, Splunk “


La anterior es una versión abreviada; consulte aquí más prácticas recomendadas y ejemplos

Puede detectar excepciones no detectadas, pero es de uso limitado. Ver http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-443c-928d-4e1ecbdd56cb

monit, forever o upstart se puede utilizar para reiniciar el proceso del nodo cuando falla. Lo mejor que puede esperar es un cierre ordenado (por ejemplo, guardar todos los datos en memoria en un controlador de excepciones no detectado).

Sección de Reseñas y Valoraciones

Recuerda dar visibilidad a este artículo si si solucionó tu problema.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *