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