Cómo construir un sistema de manejo de errores de Node.js
Publicado: 2022-03-11No es difícil ver que algunas personas tienen dificultades para manejar los errores, y algunas incluso los pasan por alto por completo. Manejar correctamente los errores significa no solo reducir el tiempo de desarrollo al encontrar fallas y errores fácilmente, sino también desarrollar una base de código robusta para aplicaciones a gran escala.
En particular, los desarrolladores de Node.js a veces se encuentran trabajando con código no tan limpio mientras manejan varios tipos de errores, aplicando incorrectamente la misma lógica en todas partes para lidiar con ellos. Simplemente se preguntan a sí mismos "¿Es Node.js malo en el manejo de errores?" o si no, ¿cómo manejarlos?” Mi respuesta a ellos es “No, Node.js no está nada mal. Eso depende de nosotros los desarrolladores”.
Aquí está una de mis soluciones favoritas para eso.
Tipos de errores en Node.js
En primer lugar, es necesario tener una comprensión clara de los errores en Node.js. En general, los errores de Node.js se dividen en dos categorías distintas: errores operativos y errores del programador .
- Los errores operativos representan problemas de tiempo de ejecución cuyos resultados son esperados y deben tratarse de manera adecuada. Los errores operativos no significan que la aplicación en sí tenga errores, pero los desarrolladores deben manejarlos cuidadosamente. Los ejemplos de errores operativos incluyen "sin memoria", "una entrada no válida para un punto final de API", etc.
- Los errores del programador representan errores inesperados en un código mal escrito. Significan que el código en sí tiene algunos problemas que resolver y se codificó incorrectamente. Un buen ejemplo es tratar de leer una propiedad de "indefinido". Para solucionar el problema, se debe cambiar el código. Ese es un error que cometió un desarrollador, no un error operativo.
Con eso en mente, no debería tener problemas para distinguir entre estas dos categorías de errores: los errores operativos son una parte natural de una aplicación y los errores del programador son errores causados por los desarrolladores. Una pregunta lógica que sigue es: "¿Por qué es útil dividirlos en dos categorías y tratar con ellos?"
Sin una comprensión clara de los errores, es posible que tenga ganas de reiniciar una aplicación cada vez que se produce un error. ¿Tiene sentido reiniciar una aplicación debido a errores de "Archivo no encontrado" cuando miles de usuarios disfrutan de la aplicación? Absolutamente no.
Pero, ¿qué pasa con los errores del programador? ¿Tiene sentido mantener una aplicación en ejecución cuando aparece un error desconocido que podría provocar un efecto de bola de nieve inesperado en la aplicación? De nuevo, ¡definitivamente no!
Es hora de manejar los errores correctamente
Suponiendo que tenga algo de experiencia con JavaScript asíncrono y Node.js, es posible que haya experimentado inconvenientes al usar devoluciones de llamada para tratar errores. Lo obligan a verificar los errores hasta los anidados, lo que provoca problemas notorios de "devolución de llamadas" que dificultan el seguimiento del flujo del código.
El uso de promesas o async/await es un buen reemplazo para las devoluciones de llamada. El flujo de código típico de async/await tiene el siguiente aspecto:
const doAsyncJobs = async () => { try { const result1 = await job1(); const result2 = await job2(result1); const result3 = await job3(result2); return await job4(result3); } catch (error) { console.error(error); } finally { await anywayDoThisJob(); } }
El uso del objeto de error incorporado de Node.js es una buena práctica porque incluye información intuitiva y clara sobre errores como StackTrace, del que dependen la mayoría de los desarrolladores para realizar un seguimiento de la raíz de un error. Y las propiedades significativas adicionales, como el código de estado HTTP y una descripción al extender la clase Error, lo harán más informativo.
class BaseError extends Error { public readonly name: string; public readonly httpCode: HttpStatusCode; public readonly isOperational: boolean; constructor(name: string, httpCode: HttpStatusCode, description: string, isOperational: boolean) { super(description); Object.setPrototypeOf(this, new.target.prototype); this.name = name; this.httpCode = httpCode; this.isOperational = isOperational; Error.captureStackTrace(this); } } //free to extend the BaseError class APIError extends BaseError { constructor(name, httpCode = HttpStatusCode.INTERNAL_SERVER, isOperational = true, description = 'internal server error') { super(name, httpCode, isOperational, description); } }
Solo implementé algunos códigos de estado HTTP en aras de la simplicidad, pero puede agregar más más adelante.
export enum HttpStatusCode { OK = 200, BAD_REQUEST = 400, NOT_FOUND = 404, INTERNAL_SERVER = 500, }
No es necesario extender BaseError o APIError, pero está bien extenderlo para errores comunes según sus necesidades y preferencias personales.
class HTTP400Error extends BaseError { constructor(description = 'bad request') { super('NOT FOUND', HttpStatusCode.BAD_REQUEST, true, description); } }
Entonces, ¿cómo se usa? Solo agrega esto:
... const user = await User.getUserById(1); if (user === null) throw new APIError( 'NOT FOUND', HttpStatusCode.NOT_FOUND, true, 'detailed explanation' );
Manejo de errores centralizado de Node.js
Ahora, estamos listos para construir el componente principal de nuestro sistema de manejo de errores Node.js: el componente de manejo de errores centralizado.
Por lo general, es una buena idea construir un componente de manejo de errores centralizado para evitar posibles duplicaciones de código al manejar errores. El componente de manejo de errores está a cargo de hacer que los errores detectados sean comprensibles, por ejemplo, enviando notificaciones a los administradores del sistema (si es necesario), transfiriendo eventos a un servicio de monitoreo como Sentry.io y registrándolos.
Aquí hay un flujo de trabajo básico para tratar los errores:
En algunas partes del código, los errores se capturan para transferirlos a un middleware de manejo de errores.

... try { userService.addNewUser(req.body).then((newUser: User) => { res.status(200).json(newUser); }).catch((error: Error) => { next(error) }); } catch (error) { next(error); } ...
El middleware de manejo de errores es un buen lugar para distinguir entre tipos de errores y enviarlos al componente centralizado de manejo de errores. Sin duda, sería útil conocer los conceptos básicos sobre el manejo de errores en el middleware Express.js.
app.use(async (err: Error, req: Request, res: Response, next: NextFunction) => { if (!errorHandler.isTrustedError(err)) { next(err); } await errorHandler.handleError(err); });
A estas alturas, uno puede imaginar cómo debería verse el componente centralizado porque ya hemos utilizado algunas de sus funciones. Tenga en cuenta que depende totalmente de usted cómo implementarlo, pero podría parecerse a lo siguiente:
class ErrorHandler { public async handleError(err: Error): Promise<void> { await logger.error( 'Error message from the centralized error-handling component', err, ); await sendMailToAdminIfCritical(); await sendEventsToSentry(); } public isTrustedError(error: Error) { if (error instanceof BaseError) { return error.isOperational; } return false; } } export const errorHandler = new ErrorHandler();
A veces, la salida del "console.log" predeterminado dificulta el seguimiento de los errores. Más bien, podría ser mucho mejor imprimir los errores de forma formateada para que los desarrolladores puedan comprender rápidamente los problemas y asegurarse de que se solucionen.
En general, esto ahorrará tiempo a los desarrolladores, lo que facilitará el seguimiento de los errores y su manejo al aumentar su visibilidad. Es una buena decisión emplear un registrador personalizable como winston o morgan.
Aquí hay un registrador Winston personalizado:
const customLevels = { levels: { trace: 5, debug: 4, info: 3, warn: 2, error: 1, fatal: 0, }, colors: { trace: 'white', debug: 'green', info: 'green', warn: 'yellow', error: 'red', fatal: 'red', }, }; const formatter = winston.format.combine( winston.format.colorize(), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.splat(), winston.format.printf((info) => { const { timestamp, level, message, ...meta } = info; return `${timestamp} [${level}]: ${message} ${ Object.keys(meta).length ? JSON.stringify(meta, null, 2) : '' }`; }), ); class Logger { private logger: winston.Logger; constructor() { const prodTransport = new winston.transports.File({ filename: 'logs/error.log', level: 'error', }); const transport = new winston.transports.Console({ format: formatter, }); this.logger = winston.createLogger({ level: isDevEnvironment() ? 'trace' : 'error', levels: customLevels.levels, transports: [isDevEnvironment() ? transport : prodTransport], }); winston.addColors(customLevels.colors); } trace(msg: any, meta?: any) { this.logger.log('trace', msg, meta); } debug(msg: any, meta?: any) { this.logger.debug(msg, meta); } info(msg: any, meta?: any) { this.logger.info(msg, meta); } warn(msg: any, meta?: any) { this.logger.warn(msg, meta); } error(msg: any, meta?: any) { this.logger.error(msg, meta); } fatal(msg: any, meta?: any) { this.logger.log('fatal', msg, meta); } } export const logger = new Logger();
Básicamente, lo que proporciona es iniciar sesión en varios niveles diferentes de forma formateada, con colores claros e iniciar sesión en diferentes medios de salida según el entorno de tiempo de ejecución. Lo bueno de esto es que puede ver y consultar registros utilizando las API integradas de Winston. Además, puede usar una herramienta de análisis de registro para analizar los archivos de registro formateados para obtener información más útil sobre la aplicación. Es increíble, ¿no?
Hasta este punto, discutimos principalmente cómo lidiar con los errores operativos. ¿Qué hay de los errores del programador? La mejor manera de lidiar con estos errores es fallar de inmediato y reiniciar correctamente con un reinicio automático como PM2; la razón es que los errores del programador son inesperados, ya que son errores reales que pueden causar que la aplicación termine en un estado y comportamiento incorrectos. de una manera inesperada.
process.on('uncaughtException', (error: Error) => { errorHandler.handleError(error); if (!errorHandler.isTrustedError(error)) { process.exit(1); } });
Por último, pero no menos importante, voy a mencionar cómo lidiar con excepciones y rechazos de promesas no controladas.
Es posible que pase mucho tiempo lidiando con promesas cuando trabaja en aplicaciones Node.js/Express. No es difícil ver mensajes de advertencia sobre rechazos de promesas no manejados cuando se olvida de manejar los rechazos.
Los mensajes de advertencia no hacen mucho más que registrar, pero es una buena práctica usar un respaldo decente y suscribirse a process.on('unhandledRejection', callback)
.
El flujo típico de manejo de errores podría parecerse al siguiente:
// somewhere in the code ... User.getUserById(1).then((firstUser) => { if (firstUser.isSleeping === false) throw new Error('He is not sleeping!'); }); ... // get the unhandled rejection and throw it to another fallback handler we already have. process.on('unhandledRejection', (reason: Error, promise: Promise<any>) => { throw reason; }); process.on('uncaughtException', (error: Error) => { errorHandler.handleError(error); if (!errorHandler.isTrustedError(error)) { process.exit(1); } });
Terminando
Cuando todo esté dicho y hecho, debe darse cuenta de que el manejo de errores no es un extra opcional sino una parte esencial de una aplicación, tanto en la etapa de desarrollo como en la producción.
La estrategia de manejar errores en un solo componente en Node.js garantizará que los desarrolladores ahorren un tiempo valioso y escriban código limpio y mantenible al evitar la duplicación de código y la falta de contexto de error.
Espero que haya disfrutado leyendo este artículo y que haya encontrado útil el flujo de trabajo y la implementación del manejo de errores discutidos para construir una base de código sólida en Node.js.
Lecturas adicionales en el blog de ingeniería de Toptal:
- Uso de rutas Express.js para el manejo de errores basado en promesas