Manejo de errores en Nodejs como un profesional
El manejo de errores en Nodejs es uno de los aspectos más importantes para esta y cualquier aplicación de grado de producción. Cualquiera puede codificar los casos de éxito. Solo los verdaderos profesionales se encargan de los casos de error.
Hoy vamos a aprender precisamente eso. ¡Vamos allá!
Primero, tenemos que entender que no todos los errores son iguales. Veamos cuántos tipos de errores pueden ocurrir en una aplicación.
- Error generado por el usuario
- Fallo de hardware
- Error de tiempo de ejecución
- Error de la base de datos
Veremos cómo podemos manejar fácilmente estos diferentes tipos de errores en Nodejs.
Obtenga una aplicación express básica
Ejecute el siguiente comando para obtener una aplicación express básica creada con mecanografiado.
git clone https://github.com/Mohammad-Faisal/express-typescript-skeleton.git
Manejar errores de URL no encontrados
¿Cómo detecta si una URL de acceso no está activa en su aplicación Express? Tiene una URL como /users,
pero alguien está presionando /user.
Necesitamos informarles que la URL a la que intentan acceder no existe.
Eso es fácil de hacer en ExpressJS. Después de definir todas las rutas, agregue el siguiente código para capturar todas las rutas no coincidentes y enviar una respuesta de error adecuada.
app.use("*", (req: Request, res: Response) => { const err = Error(`Requested path ${req.path} not found`); res.status(404).send({ success: false, message: "Requested path ${req.path} not found", stack: err.stack, }); });
Aquí estamos usando “*”
como comodín para capturar todas las rutas que no pasaron por nuestra aplicación.
Manejar todos los errores con un middleware especial
Ahora tenemos un middleware especial en Express que maneja todos los errores por nosotros. Tenemos que incluirlo al final de todas las rutas y pasar todos los errores desde el nivel superior para que este middleware pueda manejarlos por nosotros.
Lo más importante que debe hacer es mantener este middleware después de todos los demás middleware y definiciones de ruta porque, de lo contrario, algunos errores desaparecerán.
Vamos a agregarlo a nuestro archivo de índice.
app.use((err: Error, req: Request, res: Response, next: NextFunction) => { const statusCode = 500; res.status(statusCode).send({ success: false, message: err.message, stack: err.stack, }); });
Eche un vistazo a la firma de middleware. Quite la línea de otro middleware. Este middleware especial tiene un parámetro adicional llamado err
, que es del tipo Error
. Esto viene como el primer parámetro.
Y modifique nuestro código anterior para transmitir el error como el siguiente.
app.use("*", (req: Request, res: Response, next: NextFunction) => { const err = Error(`Requested path ${req.path} not found`); next(err); });
Ahora, si llegamos a una URL aleatoria, algo así como http://localhost:3001/posta
obtendremos una respuesta de error adecuada con la pila.
{ "success": false, "message": "Requested path ${req.path} not found", "stack": "Error: Requested path / not found\n at /Users/mohammadfaisal/Documents/learning/express-typescript-skeleton/src/index.ts:23:15\n" }
Objeto de error personalizado
Echemos un vistazo más de cerca al objeto de error predeterminado proporcionado por Node.js.
interface Error { name: string; message: string; stack?: string; }
Entonces, cuando arrojas un error como el siguiente.
throw new Error("Some message");
Entonces solo obtiene el nombre y las propiedades de stack
opcionales con él. Esta pila nos proporciona información sobre dónde se produjo exactamente el error. No queremos incluirlo en la producción. Veremos cómo hacerlo más adelante.
Pero es posible que deseemos agregar más información al objeto de error en sí.
Además, es posible que queramos diferenciar entre varios objetos de error.
Diseñemos una clase de error personalizada básica para nuestra aplicación.
export class ApiError extends Error { statusCode: number; constructor(statusCode: number, message: string) { super(message); this.statusCode = statusCode; Error.captureStackTrace(this, this.constructor); } }
Observe la siguiente línea.
Error.captureStackTrace(this, this.constructor);
Ayuda a capturar el seguimiento de la pila del error desde cualquier lugar de la aplicación.
En esta clase simple, también podemos agregar el statusCode
. Modifiquemos nuestro código anterior como el siguiente.
app.use("*", (req: Request, res: Response, next: NextFunction) => { const err = new ApiError(404, `Requested path ${req.path} not found`); next(err); });
Y aproveche también la nueva propiedad statusCode
en el middleware del controlador de errores.
app.use((err: ApiError, req: Request, res: Response, next: NextFunction) => { const statusCode = err.statusCode || 500; // <- Look here res.status(statusCode).send({ success: false, message: err.message, stack: err.stack, }); });
Tener una clase de error personalizada hace que su API sea predecible para los usuarios finales. La mayoría de los novatos se pierden esta parte.
Manejemos los errores de la aplicación
Ahora arrojemos un error personalizado desde dentro de nuestras rutas también.
app.get("/protected", async (req: Request, res: Response, next: NextFunction) => { try { throw new ApiError(401, "You are not authorized to access this!"); // <- fake error } catch (err) { next(err); } });
Esta es una situación creada artificialmente en la que necesitamos arrojar un error. En la vida real, podemos tener muchas situaciones en las que necesitamos usar este tipo de bloque try/catch para detectar errores.
Si presionamos la siguiente URL http://localhost:3001/protected
, obtendremos la siguiente respuesta.
{ "success": false, "message": "You are not authorized to access this!", "stack": "Some details" }
¡Así que nuestra respuesta de error está funcionando correctamente!
¡Mejoremos en esto!
Así que ahora podemos manejar nuestros errores personalizados desde cualquier parte de la aplicación. Pero requiere un bloque try catch en todas partes y requiere llamar a la función next
con el objeto de error.
Esto no es ideal. Hará que nuestro código se vea mal en poco tiempo.
Vamos a crear una función contenedora personalizada que capture todos los errores y llame a la siguiente función desde un lugar central.
¡Creemos una utilidad contenedora para este propósito!
import { Request, Response, NextFunction } from "express";export const asyncWrapper = (fn: any) => (req: Request, res: Response, next: NextFunction) => { Promise.resolve(fn(req, res, next)).catch((err) => next(err)); };
Y usarlo dentro de nuestro enrutador.
import { asyncWrapper } from "./utils/asyncWrapper";app.get( "/protected", asyncWrapper(async (req: Request, res: Response) => { throw new ApiError(401, "You are not authorized to access this!"); }) );
Ejecute el código y vea que tenemos los mismos resultados. ¡Esto nos ayuda a deshacernos de todos los bloques try/catch y llamar a la siguiente función en todas partes!
Ejemplo de un error personalizado
Podemos ajustar nuestros errores a nuestras necesidades. Vamos a crear una nueva clase de error para las rutas no encontradas.
export class NotFoundError extends ApiError { constructor(path: string) { super(404, `The requested path ${path} not found!`); } }
Y simplifique nuestro controlador de rutas incorrectas.
app.use((req: Request, res: Response, next: NextFunction) => next(new NotFoundError(req.path)));
¿Qué tan limpio es eso?
Ahora instalemos un pequeño paquete para evitar escribir los códigos de estado nosotros mismos.
yarn add http-status-codes
Y agregue el código de estado de una manera significativa.
export class NotFoundError extends ApiError { constructor(path: string) { super(StatusCodes.NOT_FOUND, `The requested path ${path} not found!`); } }
Y dentro de nuestra ruta así.
app.get( "/protected", asyncWrapper(async (req: Request, res: Response) => { throw new ApiError(StatusCodes.UNAUTHORIZED, "You are not authorized to access this!"); }) );
Simplemente hace que nuestro código sea un poco mejor.
Manejar los errores del programador.
La mejor manera de lidiar con los errores del programador es reiniciar correctamente. Coloque la siguiente línea de código al final de su aplicación. Se invocará en caso de que algo no se detecte en el middleware de error.
process.on("uncaughtException", (err: Error) => { console.log(err.name, err.message); console.log("UNCAUGHT EXCEPTION! 💥 Shutting down..."); process.exit(1); });
Manejar rechazos de promesas no manejados.
Podemos registrar el motivo del rechazo de la promesa. Estos errores nunca llegan a nuestro controlador de errores rápido. Por ejemplo, si queremos acceder a una base de datos con la contraseña incorrecta.
process.on("unhandledRejection", (reason: Error, promise: Promise<any>) => { console.log(reason.name, reason.message); console.log("UNHANDLED REJECTION! 💥 Shutting down..."); process.exit(1); throw reason; });
Mejoramiento adicional
Creemos una nueva clase ErrorHandler para manejar los errores en un lugar central.
import { Request, Response, NextFunction } from "express"; import { ApiError } from "./ApiError";export default class ErrorHandler { static handle = () => { return async (err: ApiError, req: Request, res: Response, next: NextFunction) => { const statusCode = err.statusCode || 500; res.status(statusCode).send({ success: false, message: err.message, rawErrors: err.rawErrors ?? [], stack: err.stack, }); }; }; }
Este es solo un middleware de controlador de errores simple. Puede agregar su lógica personalizada aquí. Y utilícelo dentro de nuestro archivo de índice.
app.use(ErrorHandler.handle());
Así es como podemos separar las preocupaciones respetando el principio de responsabilidad única de SOLID.
Espero que haya aprendido algo nuevo hoy.
Si le interesa, puede echar un vistazo a algunos de los otros artículos que he escrito recientemente sobre Laravel:
- Hoja de ruta para desarrolladores de Nodejs 2022
- Simulando el entorno de AWS localmente con AWS Localstack
¡Que tenga un maravilloso día!