¿Por qué necesitas un manejo de errores centralizado?
En aplicaciones Express pequeñas, es tentador manejar errores directamente en cada ruta con bloques try/catch o callbacks de error. Sin embargo, a medida que tu API crece, este enfoque genera múltiples problemas:
- Inconsistencia: Cada desarrollador puede responder con formatos diferentes
- Duplicación de código: La misma lógica de manejo se repite en múltiples lugares
- Difícil mantenimiento: Cambiar el formato de errores requiere modificar múltiples archivos
- Errores no controlados: Errores inesperados pueden crashear el servidor
La arquitectura del sistema de errores
Un sistema de manejo de errores centralizado en Express se compone de tres partes fundamentales:
- Clase de error personalizado: Una clase base que extiende
Errorcon propiedades adicionales como código de estado HTTP, código de error interno y metadatos. - Middleware de errores: Una función con la firma de cuatro parámetros (
err,req,res,next) que procesa todos los errores de manera uniforme. - Helper functions: Funciones utilitarias para lanzar errores rápidamente en diferentes situaciones.
Creando una clase de error personalizada
El primer paso es crear una clase que extienda Error para tener control total sobre las propiedades del error:
// src/errors/AppError.js
class AppError extends Error {
constructor(message, statusCode, errorCode = 'INTERNAL_ERROR') {
super(message);
this.statusCode = statusCode;
this.errorCode = errorCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;isOperational es útil para distinguir entre errores que tú creas intencionalmente (operacionales) y errores de programación o librerías externas. En producción, podrías querer registrar solo los errores no operacionales.Clases de error específicas
Ahora crea clases específicas para diferentes tipos de errores comunes:
// src/errors/ApiError.js
const AppError = require('./AppError');
class ValidationError extends AppError {
constructor(message, errors = []) {
super(message, 400, 'VALIDATION_ERROR');
this.errors = errors;
}
}
class NotFoundError extends AppError {
constructor(resource = 'Recurso') {
super(`${resource} no encontrado`, 404, 'NOT_FOUND');
}
}
class UnauthorizedError extends AppError {
constructor(message = 'No autorizado') {
super(message, 401, 'UNAUTHORIZED');
}
}
class ForbiddenError extends AppError {
constructor(message = 'Acceso denegado') {
super(message, 403, 'FORBIDDEN');
}
}
class ConflictError extends AppError {
constructor(message = 'Conflicto de recursos') {
super(message, 409, 'CONFLICT');
}
}
module.exports = {
AppError,
ValidationError,
NotFoundError,
UnauthorizedError,
ForbiddenError,
ConflictError
};throw new NotFoundError('Usuario') en vez de throw new AppError('Usuario no encontrado', 404, 'USER_NOT_FOUND'), haciendo el código más legible.Middleware de manejo de errores
El middleware de errores es el corazón del sistema. Debe ser el último middleware registrado y manejar todos los errores de manera consistente:
// src/middleware/errorHandler.js
const { AppError } = require('../errors/ApiError');
const sendErrorDev = (err, res) => {
res.status(err.statusCode).json({
success: false,
error: {
code: err.errorCode,
message: err.message,
stack: err.stack,
...(err.errors && { details: err.errors })
}
});
};
const sendErrorProd = (err, res) => {
// Error operacional: enviar detalles al cliente
if (err.isOperational) {
res.status(err.statusCode).json({
success: false,
error: {
code: err.errorCode,
message: err.message,
...(err.errors && { details: err.errors })
}
});
} else {
// Error de programación: no enviar detalles
console.error('ERROR 💥:', err);
res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'Algo salió mal. Por favor, intenta más tarde.'
}
});
}
};
const errorHandler = (err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
err.errorCode = err.errorCode || 'INTERNAL_ERROR';
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, res);
} else {
sendErrorProd(err, res);
}
};
module.exports = errorHandler;(err, req, res, next). Express identifica un middleware como de manejo de errores por la presencia de cuatro parámetros, siendo el primero el error.Manejo de errores asíncronos
Express 4 y versiones posteriores manejan automáticamente errores rechazados en funciones async, pero es buena práctica usar un wrapper para capturar todos los errores posibles:
// src/utils/catchAsync.js
const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
module.exports = catchAsync;Uso en tus controladores:
// src/controllers/userController.js
const catchAsync = require('../utils/catchAsync');
const { NotFoundError } = require('../errors/ApiError');
const User = require('../models/User');
exports.getUser = catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new NotFoundError('Usuario'));
}
res.status(200).json({
success: true,
data: user
});
});catchAsync, el método .catch(next) captura la promesa rechazada y la pasa al siguiente middleware de errores automáticamente.Manejo de errores de Mongoose
Mongoose genera errores específicos cuando hay problemas de validación o cuando no encuentra documentos. Debes transformar estos errores al formato de tu API:
// src/middleware/mongooseErrorHandler.js
const mongoose = require('mongoose');
const { ValidationError, NotFoundError } = require('../errors/ApiError');
const handleMongooseError = (err) => {
// Error de validación de Mongoose
if (err instanceof mongoose.Error.ValidationError) {
const errors = Object.values(err.errors).map(e => ({
field: e.path,
message: e.message
}));
return new ValidationError('Error de validación', errors);
}
// Error de CastError (ID malformado)
if (err instanceof mongoose.Error.CastError) {
return new ValidationError(`Valor '${err.value}' inválido para el campo '${err.path}'`);
}
// Error de duplicado de MongoDB
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
return new ValidationError(`El campo '${field}' ya existe`);
}
return err;
};
module.exports = handleMongooseError;const errorHandler = (err, req, res, next) => {
// ... código anterior ...
let error = err;
// Convertir errores de Mongoose
if (err.name === 'MongoError' || err.name === 'CastError' || err.name === 'ValidationError') {
error = handleMongooseError(err);
}
// Continuar con el manejo normal
sendError(error, res);
};404 y errores de ruta
No olvides manejar las rutas que no existen:
// src/middleware/notFound.js
const { NotFoundError } = require('../errors/ApiError');
const notFoundHandler = (req, res, next) => {
next(new NotFoundError(`Ruta ${req.originalUrl} no encontrada`));
};
module.exports = notFoundHandler;Regístralo después de todas tus rutas:
// src/app.js
const express = require('express');
const app = express();
const errorHandler = require('./middleware/errorHandler');
const notFoundHandler = require('./middleware/notFound');
// ... tus rutas ...
app.use(notFoundHandler);
app.use(errorHandler);
module.exports = app;Middleware de desarrollo vs producción
Es crucial tener diferentes comportamientos según el entorno:
| Aspecto | Desarrollo | Producción |
|---|---|---|
| Mensaje de error | Detallado con stack trace | Genérico y seguro |
| Logs | Console.log básico | Logs estructurados con Winston o similar |
| CORS | Permisivo | Restrictivo |
Logging de errores
Para producción, usa un sistema de logging que no bloquee el event loop:
// src/utils/logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: 'error',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
module.exports = logger;Ver más: Integración de logging en el handler de errores// Actualiza errorHandler.js
const logger = require('../utils/logger');
const errorHandler = (err, req, res, next) => {
// ... código anterior ...
if (process.env.NODE_ENV === 'production' && !err.isOperational) {
logger.error({
message: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip,
userAgent: req.get('user-agent')
});
}
};Manejo de errores en operaciones asíncronas批量
Cuando trabajas con múltiples operaciones asíncronas, usa Promise.all y captura el primer error:
exports.processUserData = catchAsync(async (req, res, next) => {
const userData = req.body;
// Validar datos primero
if (!userData.email || !userData.name) {
return next(new ValidationError('Email y nombre son requeridos'));
}
// Ejecutar múltiples operaciones
const [user, profile, settings] = await Promise.all([
User.create(userData),
Profile.create({ userId: userData.id }),
Settings.create({ userId: userData.id })
]);
res.status(201).json({ success: true, data: { user, profile, settings } });
});"Un sistema de manejo de errores bien diseñado no se trata solo de capturar errores, sino de proporcionar información útil tanto para depurar problemas como para que los clientes de tu API sepan exactamente qué salió mal y cómo resolverlo."
Resumen de la estructura final
src/
├── errors/
│ ├── AppError.js # Clase base
│ └── ApiError.js # Clases específicas
├── middleware/
│ ├── errorHandler.js # Handler principal
│ ├── notFound.js # 404 handler
│ └── mongooseErrorHandler.js
├── utils/
│ ├── catchAsync.js # Wrapper async
│ └── logger.js # Sistema de logs
└── app.js # Configuración final¿Cuál es la firma correcta del middleware de manejo de errores en Express?
- A) (err, req, res)
- B) (req, res, next)
- C) (err, req, res, next)
- D) (error, request, response)
¿Por qué es importante marcar errores como 'operacionales' vs 'no operacionales'?
- A) Para que el código sea más rápido
- B) Para distinguir entre errores del programador y errores esperados, permitiendo diferente tratamiento
- C) No tiene importancia, solo es un ejemplo decorativo
- D) Para cumplir con estándares de ECMAScript