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

755

Acabo de empezar a probar node.js hace unos días. Me di cuenta de que el Nodo se termina cada vez que tengo una excepción no controlada en mi programa. Esto es diferente al contenedor de servidor normal al que he estado expuesto, donde solo el subproceso de trabajo muere cuando se producen excepciones no controladas y el contenedor aún podría recibir la solicitud. Esto plantea algunas preguntas:

  • ¿Es process.on('uncaughtException')la única forma efectiva de protegerse contra ella?
  • ¿ process.on('uncaughtException')Capturará también la excepción no controlada durante la ejecución de procesos asincrónicos?
  • ¿Hay un módulo que ya está construido (como enviar un correo electrónico o escribir en un archivo) que podría aprovechar en el caso de excepciones no detectadas?

Agradecería cualquier puntero / artículo que me mostrara las mejores prácticas comunes para manejar excepciones no capturadas en node.js

momo
fuente
11
las excepciones no capturadas no deberían suceder. Si utilizan un programa que reinicia toda la aplicación cuando se bloquea (nodemon, forever, supervisor)
Raynos
116
Las excepciones no detectadas siempre pueden ocurrir a menos que coloque cada parte de su código asincrónico en el interior try .. catch, y verifique que esto también se haga para todas sus bibliotecas
Dan
13
+1 Dan Al principio pensé que todas tus librerías eran un poco exageradas, ya que "solo" necesitas envolver todos tus "puntos de entrada de hilo" en el código en try / catches. Pero al pensarlo con más cuidado, cualquier lib podría tener un setTimeouto setIntervalalgo por el estilo enterrado en algún lugar profundo que no pueda ser captado por su código.
Eugene Beresovsky
8
@EugeneBeresovksy Dan tiene razón, pero no cambia el hecho de que cuando se producen excepciones no capturadas, la única opción segura es reiniciar la aplicación. En otras palabras, su aplicación se ha bloqueado y no hay nada que pueda hacer o deba hacer al respecto. Si desea hacer algo constructivo, implemente la nueva, y aún experimental, característica de dominio v0.8 para que pueda registrar el bloqueo y enviar una respuesta 5xx a su cliente.
ostergaard el
1
@Dan Incluso incluir todas las funciones de devolución de llamada en try .. catch no garantiza errores de captura. En el caso de que un módulo requerido use sus propios binarios, pueden bloquearse sin gracia. Me ha sucedido esto con phantomjs-node, fallando en errores que son imposibles de detectar (a menos que hiciera algún tipo de inspección de proceso en los archivos binarios requeridos, pero nunca busqué eso).
Trindaz

Respuestas:

739

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 los errores no detectados tanto como sea posible, como tal, en lugar de arrojar literalmente el error, podemos "arrojar" el error de forma segura utilizando uno de los siguientes métodos, dependiendo de nuestra arquitectura de código:

  • Para el código sincrónico, 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 la devolución de llamada (es decir, asíncrono), el primer argumento de la devolución de llamada es err, si ocurre un error erres el error, si no ocurre un error, entonces erres null. Cualquier otro argumento sigue el errargumento:

    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 el código lleno de eventos , donde el error puede ocurrir en cualquier lugar, en lugar de lanzar el error, active el errorevento 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)

Errores de "captura" seguros

Sin embargo, a veces todavía puede haber código que arroje un error en algún lugar que pueda conducir a una excepción no detectada y a un posible bloqueo de nuestra aplicación si no la 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 se produce el error, podemos ajustar 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 un código síncrono, y por alguna razón no podemos usar dominios (quizás una versión antigua 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 usar try...catchen código asíncrono, ya que no se detectará un error asincrónicamente lanzado:

    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 desea trabajar try..catchjunto con el código asincrónico, cuando ejecute el Nodo 7.4 o superior, puede usarlo de async/awaitforma nativa para escribir sus funciones asincrónicas.

    Otra cosa con la que debe tener cuidado try...catches el riesgo de incluir su devolución de llamada de finalización dentro de la trydeclaración de la siguiente manera:

    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 ya 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 el código asincrónico (2) la ejecución de captura de prueba de captura que no desea. En los lenguajes que permiten un subproceso adecuado en lugar del estilo de máquina de eventos asíncrono de JavaScript, esto no es un problema.

  • Finalmente, en el caso de que ocurra un error no detectado en un lugar que no estaba envuelto en un dominio o una declaración de captura de prueba, podemos hacer que nuestra aplicación no se bloquee al usar el uncaughtExceptionescucha (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
balupton
fuente
55
Gracias Raynos, actualizado. ¿Tienes una fuente que explica los males de try catch? Como me encantaría respaldar eso con evidencia. También se corrigió el ejemplo de sincronización.
balupton
2
Esta respuesta ya no es válida. Dominios resuelve este problema (recomendado por Node.js)
Gabriel Llamas
55
@balupton Deben arrojarse errores para el manejo de errores. Definitivamente NO deben ser evitados. No hay nada en ellos que interrumpa la ejecución de la aplicación o cualquier otra cosa. Java y la mayoría de los otros lenguajes modernos tienen un excelente soporte para excepciones. Mi única conclusión después de leer algunas de las publicaciones erróneas aquí es que las personas no las entienden muy bien y, por lo tanto, les tienen miedo. Miedo duda incierta. Este debate se decidió de manera concluyente a favor de excepciones al menos hace 20 años.
enl8enmentnow
22
Ahora los dominios están en desuso por io.js : " Este módulo está pendiente de desuso. Una vez que se haya finalizado una API de reemplazo, este módulo quedará totalmente en desuso ... Los usuarios que deben tener la funcionalidad que proporcionan los dominios pueden confiar en él por el momento, pero debería esperar tener que migrar a una solución diferente en el futuro ".
Timothy Gu
55
¿La API del dominio está en desuso ahora ? Mencionan una API de reemplazo: ¿alguien sabe cuándo saldrá esto y cómo se verá?
UpTheCreek
95

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


Mejores prácticas de manejo de errores 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 la forma más rápida de ir al infierno (también conocida como la pirámide de la fatalidad). El mejor regalo que puede darle a su código es usar una biblioteca de promesas confiable que proporcione una sintaxis de código muy compacta y familiar como try-catch

De lo contrario: el estilo de devolución de llamada 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, anidamiento excesivo 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, en el puesto 11 de 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 pila es muy parecido a conducir un automóvil sin pedal de freno: usted no nos damos cuenta de cuánto lo necesita, hasta que lo alcanza 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 tú hay que saber utilizar las promesas correctamente para aprovecharlas " .


Número2: use solo el objeto de 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 de error incorporado 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 de qué tipo de errores 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 puede conducir a la pérdida de información de errores críticos 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" (De la reflexión del blog, clasificó 6 para las palabras clave "Node.JS error object")

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


Número 3: Distinguir errores operacionales vs errores de programador

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

De lo contrario: siempre puede reiniciar la aplicación cuando aparece un error, pero ¿por qué decepcionar a ~ 5000 usuarios en línea debido a un error menor y pronosticado (error operativo)? lo contrario tampoco es ideal: mantener la aplicación activa cuando se produce un problema desconocido (error del programador) puede generar un comportamiento imprevisto. Diferenciar los dos permite actuar con tacto y aplicar un enfoque equilibrado basado en el 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);
});

Cita del blog : "De lo contrario, arriesga el estado" (del blog debugable, clasificado 3 para las palabras clave "Node.JS excepción no detectada")

" ... Por la propia naturaleza de cómo funciona el lanzamiento en JavaScript, casi nunca hay forma de" retomar donde lo dejaste ", 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 otra persona activó un error. El mejor enfoque es envía una respuesta de error a la solicitud que activó el error, mientras deja que los demás terminen en su horario normal y dejen de escuchar nuevas solicitudes en ese trabajador "


Número 4: maneja los errores de forma centralizada, a través del middleware, pero no dentro de este

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 que todos los puntos finales (por ejemplo, middleware Express, trabajos cron, pruebas unitarias) llaman cuando entra un error.

De lo contrario: no manejar los errores en un solo lugar conducirá a la duplicación de 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 su interlocutor" (Del blog Joyent, clasificado 1 para las palabras clave "Manejo de errores 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 interlocutor, que propaga el error a su interlocutor, 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 u 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 no puede saber en qué contexto se produjo el error "


Número5: Errores de la API de documentos usando Swagger

TL; DR: Informe a los llamadores de la API qué errores pueden aparecer 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 API podría decidir bloquearse y reiniciarse solo porque recibió un error que no pudo entender. Nota: la persona que llama de su API puede ser usted (muy típico en un entorno de microservicios)

Cita del blog: "Debe informar a las personas que llaman qué errores pueden ocurrir" (del blog Joyent, en el puesto 1 por las palabras clave "Node.JS logging")

... Hemos hablado sobre cómo manejar los errores, pero cuando está escribiendo una nueva función, ¿cómo entrega errores al código que llamó a su 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 informar 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 acerca de la salud de la aplicación. Una práctica común sugiere reiniciar el proceso cuidadosamente usando 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 locamente

Ejemplo de código: decidir si se bloquea

//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)

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


Número 7: use un registrador maduro para aumentar la visibilidad de los 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 mediante console.logs o manualmente a través de un archivo de texto desordenado sin consultar herramientas o un visor de registro decente podría mantenerlo 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. Indique la hora en cada línea de registro. Este se explica por sí mismo: debe poder saber cuándo se produjo 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, puede estar escribiendo registros de seguimiento en un archivo, pero cuando se encuentra 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 utilizando productos APM

TL; DR: los productos de monitoreo y rendimiento (también conocido como APM) evalúan de manera proactiva su base de código o API para que puedan resaltar automáticamente los errores, bloqueos y piezas lentas que faltaba

De lo contrario: es posible que gaste un gran esfuerzo en 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 el escenario del mundo real y cómo afectan esto a la experiencia de usuario

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 pueden configurar en pocos minutos. Los siguientes son algunos contendientes seleccionados: Pingdom, Uptime Robot y New Relic 2 Instrumentación de código: familia de productos que requiere incorporar un agente dentro de la aplicación para beneficiarse de la detección lenta de código, estadísticas de excepciones, monitoreo de rendimiento y muchos más. A continuación se presentan 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 fácilmente por encima del rendimiento de la aplicación. Esto generalmente implica agregar múltiples fuentes de información (registros de aplicaciones, registros de bases de datos, registros de servidores, etc.) y el trabajo de diseño inicial del tablero. Los siguientes son algunos contendientes seleccionados: Datadog, Splunk "


Lo anterior es una versión abreviada: vea aquí más prácticas recomendadas y ejemplos

Yonatan
fuente
30

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, forevero upstartse puede usar para reiniciar el proceso de nodo cuando se bloquea. Lo mejor que puede esperar es un apagado correcto (por ejemplo, guarde todos los datos en memoria en un controlador de excepciones no detectado).

nponeccop
fuente
44
+1 El enlace es útil, gracias. Todavía estoy buscando las mejores prácticas y el significado de "reinicio elegante" en el contexto de node.js
momo
Mi comprensión del "reinicio elegante" en este contexto sería esencialmente lo que sugiere nponeccop: dejar que el proceso muera y dejar que lo que se está ejecutando en primer lugar lo reinicie.
Ilkka
Muchas gracias por ese enlace! ¡Realmente util!
SatheeshJM
Esta es una respuesta genial. Sin embargo, no estaría de acuerdo en devolver un error en su primer ejemplo. Devolver un Errorhace que el valor de retorno sea polimórfico, lo que confunde la semántica de la función innecesariamente. Además, el buceo por 0 ya se maneja de JavaScript dando Infinity, -Infinityo NaN, los valores donde typeof === 'number'. Se pueden verificar con !isFinite(value). En general, recomendaría nunca devolver un error de una función. Mejor en términos de legibilidad de código y mantenimiento para arrojar o devolver un valor no polimórfico especial con semántica consistente.
wprl
El enlace está roto. downforeveryoneorjustme.com/debuggable.com
Kev
13

Los dominios de nodejs son la forma más actualizada de manejar errores en nodejs. Los dominios pueden capturar tanto errores / otros eventos como objetos arrojados tradicionalmente. Los dominios también proporcionan funcionalidad para manejar devoluciones de llamada con un error pasado como primer argumento a través del método de intercepción.

Al igual que con el manejo normal de errores de estilo try / catch, generalmente es mejor arrojar errores cuando ocurren y bloquear las áreas donde desea aislar los errores para que no afecten al resto del código. La forma de "bloquear" estas áreas es llamar a domain.run con una función como un bloque de código aislado.

En el código síncrono, lo anterior es suficiente: cuando ocurre un error, puede dejarlo pasar, o lo atrapa y lo maneja allí, revocando cualquier dato que necesite revertir.

try {  
  //something
} catch(e) {
  // handle data reversion
  // probably log too
}

Cuando el error ocurre en una devolución de llamada asincrónica, debe poder manejar completamente la reversión de datos (estado compartido, datos externos como bases de datos, etc.). O bien, debe configurar algo para indicar que ha ocurrido una excepción: donde quiera que se preocupe por ese indicador, debe esperar a que se complete la devolución de llamada.

var err = null;
var d = require('domain').create();
d.on('error', function(e) {
  err = e;
  // any additional error handling
}
d.run(function() { Fiber(function() {
  // do stuff
  var future = somethingAsynchronous();
  // more stuff

  future.wait(); // here we care about the error
  if(err != null) {
    // handle data reversion
    // probably log too
  }

})});

Parte del código anterior es feo, pero puede crear patrones para que sea más bonito, por ejemplo:

var specialDomain = specialDomain(function() {
  // do stuff
  var future = somethingAsynchronous();
  // more stuff

  future.wait(); // here we care about the error
  if(specialDomain.error()) {
    // handle data reversion
    // probably log too
  } 
}, function() { // "catch"
  // any additional error handling
});

ACTUALIZACIÓN (2013-09):

Arriba, uso un futuro que implica semántica de fibras , que le permite esperar futuros en línea. En realidad, esto le permite utilizar los bloques tradicionales try-catch para todo , lo que creo que es la mejor manera de hacerlo. Sin embargo, no siempre puede hacer esto (es decir, en el navegador) ...

También hay futuros que no requieren semántica de fibras (que luego funcionan con JavaScript normal y navegable). Estos pueden llamarse futuros, promesas o diferidos (de aquí en adelante me referiré a futuros). Las bibliotecas de futuros JavaScript simples permiten que los errores se propaguen entre futuros. Solo algunas de estas bibliotecas permiten manejar correctamente cualquier futuro lanzado, así que tenga cuidado.

Un ejemplo:

returnsAFuture().then(function() {
  console.log('1')
  return doSomething() // also returns a future

}).then(function() {
  console.log('2')
  throw Error("oops an error was thrown")

}).then(function() {
  console.log('3')

}).catch(function(exception) {
  console.log('handler')
  // handle the exception
}).done()

Esto imita un try-catch normal, a pesar de que las piezas son asíncronas. Imprimiría:

1
2
handler

Tenga en cuenta que no imprime '3' porque se produjo una excepción que interrumpe ese flujo.

Echa un vistazo a las promesas de bluebird:

Tenga en cuenta que no he encontrado muchas otras bibliotecas que no sean estas que manejan correctamente las excepciones lanzadas. El diferido de jQuery, por ejemplo, no lo hace: el manejador de "falla" nunca obtendría la excepción arrojada como un manejador de "entonces", que en mi opinión es un factor decisivo.

BT
fuente
La especificación adecuada de promesas en Javascript se conoce como Promesas / A +. Puede ver una lista de implementaciones aquí: github.com/promises-aplus/promises-spec/blob/master/… . Tenga en cuenta que un Promises / A + desnudo es inutilizable en la práctica: Promises / A + todavía deja muchos problemas prácticos para que las bibliotecas se resuelvan. Sin embargo, cosas absolutamente esenciales como la propagación de errores que muestra, el orden de ejecución determinista y la seguridad contra el desbordamiento de la pila están garantizadas.
Esailija
11

Escribí sobre esto recientemente en http://snmaynard.com/2012/12/21/node-error-handling/ . Una nueva característica del nodo en la versión 0.8 son los dominios y le permite combinar todas las formas de manejo de errores en una forma de administración más fácil. Puedes leer sobre ellos en mi publicación.

También puede usar algo como Bugsnag para rastrear sus excepciones no detectadas y recibir una notificación por correo electrónico, sala de chat o hacer que se cree un ticket para una excepción no detectada (soy el cofundador de Bugsnag).

Simon Maynard
fuente
2
El módulo de dominio ahora está oficialmente en desuso. nodejs.org/api/domain.html
MattSidor
3

Solo me gustaría agregar que la biblioteca Step.js lo ayuda a manejar las excepciones pasándolas siempre a la función del siguiente paso. Por lo tanto, puede tener como último paso una función que verifique cualquier error en cualquiera de los pasos anteriores. Este enfoque puede simplificar enormemente su manejo de errores.

A continuación hay una cita de la página de Github:

cualquier excepción lanzada se captura y se pasa como primer argumento a la siguiente función. Siempre y cuando no anide las funciones de devolución de llamada en línea con sus funciones principales, esto evita que haya excepciones no detectadas. Esto es muy importante para los servidores node.JS de larga ejecución, ya que una sola excepción no detectada puede hacer que todo el servidor se caiga.

Además, puede usar Step para controlar la ejecución de scripts para tener una sección de limpieza como último paso. Por ejemplo, si desea escribir un script de compilación en Node e informar cuánto tardó en escribir, el último paso puede hacerlo (en lugar de tratar de desenterrar la última devolución de llamada).

Michael Yagudaev
fuente
3

Una instancia en la que puede ser apropiado usar un try-catch es cuando se usa un bucle forEach. Es síncrono, pero al mismo tiempo no puede usar una declaración return en el ámbito interno. En su lugar, se puede utilizar un enfoque de prueba y captura para devolver un objeto Error en el ámbito apropiado. Considerar:

function processArray() {
    try { 
       [1, 2, 3].forEach(function() { throw new Error('exception'); }); 
    } catch (e) { 
       return e; 
    }
}

Es una combinación de los enfoques descritos por @balupton anteriormente.

Michael Yagudaev
fuente
En lugar de arrojar errores, algunos desarrolladores recomiendan usar el concepto Resultado de Rust para devolver un OK o un Fail , cuando el error es una posibilidad conocida. Esto mantiene las fallas separadas de los errores inesperados. Una implementación de JS de esto es r-result .
joeytwiddle
Es una decisión de diseño de toda la aplicación. Creo que su concepto de devolución de errores es más o menos equivalente y fácil de comenzar (sin dependencias adicionales), pero menos explícito (el resultado lo hace dolorosamente consciente de cuándo es posible que sea necesario manejar las fallas) y menos eficiente en aquellos casos en que se apila una pila construido innecesariamente.
joeytwiddle
1

Después de leer esta publicación hace algún tiempo, me preguntaba si era seguro usar dominios para el manejo de excepciones en un nivel de API / función. Quería usarlos para simplificar el código de manejo de excepciones en cada función asincrónica que escribí. Mi preocupación era que el uso de un nuevo dominio para cada función introduciría una sobrecarga significativa. Mi tarea parece indicar que hay una sobrecarga mínima y que el rendimiento es realmente mejor con dominios que con try catch en algunas situaciones.

http://www.lighthouselogic.com/#/using-a-new-domain-for-each-async-function-in-node/

Jabonoso
fuente
1

Aquí se ha discutido muy bien los errores de captura, pero vale la pena recordar cerrar la sesión en algún lugar para poder verlos y arreglarlos.

Bunyan es un marco de registro popular para NodeJS: admite escribir en varios lugares de salida diferentes, lo que lo hace útil para la depuración local, siempre que evite console.log. En el controlador de errores de su dominio, puede escupir el error a un archivo de registro.

var log = bunyan.createLogger({
  name: 'myapp',
  streams: [
    {
      level: 'error',
      path: '/var/tmp/myapp-error.log'  // log ERROR to this file
    }
  ]
});

Esto puede llevar mucho tiempo si tiene muchos errores y / o servidores para verificar, por lo que podría valer la pena buscar una herramienta como Raygun (descargo de responsabilidad, trabajo en Raygun) para agrupar los errores, o usarlos juntos. Si decidió usar Raygun como herramienta, también es bastante fácil de configurar

var raygunClient = new raygun.Client().init({ apiKey: 'your API key' });
raygunClient.send(theError);

Cruzada con el uso de una herramienta como PM2 o ​​para siempre, su aplicación debería poder bloquearse, cerrar la sesión de lo que sucedió y reiniciar sin problemas importantes.

K. Craven
fuente