Manejo de Errores
El sistema de manejo de errores de Fox Framework proporciona mecanismos robustos para capturar, gestionar y responder a errores de manera elegante en tus aplicaciones. Diseñado para ser extensible y configurado según las necesidades específicas de tu proyecto, este sistema facilita la depuración durante el desarrollo y asegura respuestas adecuadas en producción.
Características Principales
- Captura global de errores: Previene la caída de la aplicación ante errores inesperados
- Errores tipados: Jerarquía de errores con información detallada y códigos HTTP
- Formatos de respuesta flexibles: Adaptación automática al formato solicitado (HTML, JSON, XML)
- Logging integrado: Registro detallado de errores para análisis y depuración
- Soporte para validación: Integración con esquemas de validación
- Personalizable: Controladores de errores personalizados por tipo o código HTTP
- Middlewares de error: Encadenamiento de manejadores de errores
- Internacionalización: Mensajes de error en múltiples idiomas
Fundamentos del Sistema
Clase Base de Errores
Fox Framework extiende la clase Error nativa para proporcionar información adicional útil para la gestión de errores HTTP:
import { HttpStatusCode } from '@foxframework/core';
export class FoxError extends Error {
/** Código de estado HTTP */
public statusCode: number;
/** Código interno de error (opcional) */
public code?: string;
/** Detalles adicionales del error */
public details?: Record<string, any>;
/** Error original que causó esta excepción */
public originalError?: Error;
/** Indica si el error debe ser registrado */
public shouldLog: boolean;
constructor(message: string, options?: {
statusCode?: number;
code?: string;
details?: Record<string, any>;
originalError?: Error;
shouldLog?: boolean;
}) {
super(message);
this.name = this.constructor.name;
this.statusCode = options?.statusCode || HttpStatusCode.INTERNAL_SERVER_ERROR;
this.code = options?.code;
this.details = options?.details;
this.originalError = options?.originalError;
this.shouldLog = options?.shouldLog !== undefined ? options.shouldLog : true;
// Preservar stack trace
Error.captureStackTrace(this, this.constructor);
}
/**
* Convierte el error a un objeto plano para serialización
*/
toJSON(): Record<string, any> {
return {
message: this.message,
code: this.code,
statusCode: this.statusCode,
details: this.details,
// No incluimos originalError ni stack en producción
...(process.env.NODE_ENV !== 'production' ? {
stack: this.stack
} : {})
};
}
}Errores Especializados
El framework incluye una serie de errores predefinidos para casos comunes:
import { HttpStatusCode } from '@foxframework/core';
import { FoxError } from '@foxframework/core';
export class BadRequestError extends FoxError {
constructor(message = 'Petición inválida', options?: Partial<FoxErrorOptions>) {
super(message, {
statusCode: HttpStatusCode.BAD_REQUEST,
code: 'BAD_REQUEST',
...options
});
}
}
export class UnauthorizedError extends FoxError {
constructor(message = 'No autorizado', options?: Partial<FoxErrorOptions>) {
super(message, {
statusCode: HttpStatusCode.UNAUTHORIZED,
code: 'UNAUTHORIZED',
...options
});
}
}
export class ForbiddenError extends FoxError {
constructor(message = 'Acceso prohibido', options?: Partial<FoxErrorOptions>) {
super(message, {
statusCode: HttpStatusCode.FORBIDDEN,
code: 'FORBIDDEN',
...options
});
}
}
export class NotFoundError extends FoxError {
constructor(message = 'Recurso no encontrado', options?: Partial<FoxErrorOptions>) {
super(message, {
statusCode: HttpStatusCode.NOT_FOUND,
code: 'NOT_FOUND',
...options
});
}
}
export class MethodNotAllowedError extends FoxError {
constructor(message = 'Método no permitido', options?: Partial<FoxErrorOptions>) {
super(message, {
statusCode: HttpStatusCode.METHOD_NOT_ALLOWED,
code: 'METHOD_NOT_ALLOWED',
...options
});
}
}
export class ConflictError extends FoxError {
constructor(message = 'Conflicto en la operación', options?: Partial<FoxErrorOptions>) {
super(message, {
statusCode: HttpStatusCode.CONFLICT,
code: 'CONFLICT',
...options
});
}
}
export class ValidationError extends BadRequestError {
constructor(
message = 'Error de validación',
public validationErrors: Record<string, string[]> = {},
options?: Partial<FoxErrorOptions>
) {
super(message, {
code: 'VALIDATION_ERROR',
details: { validationErrors },
...options
});
}
}
export class ServiceUnavailableError extends FoxError {
constructor(message = 'Servicio no disponible', options?: Partial<FoxErrorOptions>) {
super(message, {
statusCode: HttpStatusCode.SERVICE_UNAVAILABLE,
code: 'SERVICE_UNAVAILABLE',
...options
});
}
}
export class InternalServerError extends FoxError {
constructor(message = 'Error interno del servidor', options?: Partial<FoxErrorOptions>) {
super(message, {
statusCode: HttpStatusCode.INTERNAL_SERVER_ERROR,
code: 'INTERNAL_SERVER_ERROR',
...options
});
}
}Configuración del Manejador de Errores
Configuración Básica
import { FoxFactory } from '@foxframework/core';
import { ErrorHandler } from '@foxframework/core';
// Crear un servidor con configuración del manejador de errores
const server = FoxFactory.createServer({
port: 3000,
// Configuración del manejador de errores
errorHandler: {
// Mostrar detalles de errores en respuestas
exposeErrors: process.env.NODE_ENV !== 'production',
// Vista para errores (cuando se responde con HTML)
errorView: 'errors/error',
// Vista específica para error 404
notFoundView: 'errors/404',
// Logger para errores
logger: console,
// Función para determinar si un error debe ser registrado
shouldLogError: (err) => {
// No registrar errores 404 para reducir ruido
if (err.statusCode === 404) return false;
return true;
},
// Transformador de errores antes de enviar respuesta
errorTransformer: (err, req) => {
// Añadir información de contexto a errores
return {
...err.toJSON(),
requestId: req.id,
path: req.path,
timestamp: new Date().toISOString()
};
}
},
// Resto de la configuración del servidor
router
});Middleware para Captura de Errores
Fox Framework proporciona un middleware que captura errores y los gestiona adecuadamente:
import { errorMiddleware } from '@foxframework/core';
// Registrar middleware de errores (normalmente al final de la cadena)
server.use(errorMiddleware({
exposeErrors: process.env.NODE_ENV !== 'production',
logger: console,
// Otras opciones de configuración
}));
// O usando la configuración global
server.useErrorHandler();Uso del Sistema de Errores
Lanzar Errores
En Controladores
import {
Controller,
Get,
Post,
HttpContext,
NotFoundError,
ValidationError,
UnauthorizedError
} from '@foxframework/core';
@Controller('/users')
export class UserController {
constructor(private userService) {}
@Get('/:id')
async getUser(ctx: HttpContext) {
const { id } = ctx.params;
// Validar parámetro
if (!id || isNaN(Number(id))) {
throw new ValidationError('ID de usuario no válido', {
id: ['Debe ser un número válido']
});
}
// Buscar usuario
const user = await this.userService.findById(id);
// Verificar si existe
if (!user) {
throw new NotFoundError(`Usuario con ID ${id} no encontrado`);
}
// Verificar permisos
if (!ctx.auth.can('view', user)) {
throw new UnauthorizedError('No tienes permisos para ver este usuario');
}
// Devolver usuario
return user;
}
@Post('/')
async createUser(ctx: HttpContext) {
try {
const userData = ctx.body;
// Validar datos
const validator = ctx.validator.validate(userData, userSchema);
if (!validator.isValid()) {
throw new ValidationError(
'Datos de usuario inválidos',
validator.getErrors()
);
}
// Crear usuario
const user = await this.userService.create(userData);
// Respuesta exitosa
return ctx.response.created(user);
} catch (error) {
// Capturar errores específicos
if (error.code === 'DUPLICATE_EMAIL') {
throw new ConflictError('El email ya está en uso', {
code: 'DUPLICATE_EMAIL',
details: { field: 'email' }
});
}
// Re-lanzar otros errores
throw error;
}
}
}En Servicios
import { NotFoundError, ValidationError, DatabaseError } from '@foxframework/core';
export class UserService {
constructor(private db) {}
async findById(id: number): Promise<User> {
try {
const user = await this.db.users.findUnique({
where: { id }
});
if (!user) {
throw new NotFoundError(`Usuario con ID ${id} no encontrado`, {
code: 'USER_NOT_FOUND',
details: { id }
});
}
return user;
} catch (error) {
// Transformar errores de base de datos
if (error.name === 'PrismaClientKnownRequestError') {
throw new DatabaseError('Error de base de datos', {
originalError: error,
code: `DB_ERROR_${error.code}`
});
}
throw error;
}
}
async create(data: UserCreateDto): Promise<User> {
// Validar datos
if (!data.email) {
throw new ValidationError('Datos de usuario inválidos', {
email: ['El email es obligatorio']
});
}
try {
// Verificar si el email ya existe
const existing = await this.db.users.findUnique({
where: { email: data.email }
});
if (existing) {
throw new ConflictError('El email ya está registrado', {
code: 'DUPLICATE_EMAIL',
details: { field: 'email' }
});
}
// Crear usuario
return await this.db.users.create({
data
});
} catch (error) {
if (error instanceof FoxError) {
throw error;
}
// Transformar otros errores
throw new InternalServerError('Error al crear usuario', {
originalError: error
});
}
}
}Helper en Contexto HTTP
El framework proporciona ayudantes en el contexto HTTP para simplificar el lanzamiento de errores comunes:
@Controller('/products')
export class ProductController {
@Get('/:id')
async getProduct(ctx: HttpContext) {
const { id } = ctx.params;
// Usando helpers en lugar de lanzar errores directamente
if (!id) {
return ctx.response.badRequest('ID de producto requerido');
}
const product = await this.productService.findById(id);
if (!product) {
return ctx.response.notFound(`Producto con ID ${id} no encontrado`);
}
if (product.isPrivate && !ctx.auth.isAuthenticated) {
return ctx.response.unauthorized('Necesitas iniciar sesión para ver este producto');
}
if (product.publishDate > new Date()) {
return ctx.response.forbidden('Este producto aún no está disponible');
}
return product;
}
}Personalización del Manejo de Errores
Manejadores de Error Personalizados
Puedes definir manejadores específicos para diferentes tipos de error:
import { ErrorHandler } from '@foxframework/core';
const errorHandler = new ErrorHandler({
// Configuración base
exposeErrors: process.env.NODE_ENV !== 'production',
logger: console
});
// Registrar manejador para errores de validación
errorHandler.registerHandler(ValidationError, (err, req, res) => {
const response = {
success: false,
message: err.message,
errors: err.validationErrors
};
return res.status(err.statusCode).json(response);
});
// Registrar manejador para errores de autenticación
errorHandler.registerHandler(UnauthorizedError, (err, req, res) => {
// Redirigir a login si es una petición web
const acceptsHtml = req.accepts('html');
if (acceptsHtml) {
return res.redirect(`/auth/login?returnUrl=${req.path}&error=unauthorized`);
}
// JSON para API
return res.status(err.statusCode).json({
success: false,
message: err.message,
code: err.code
});
});
// Registrar manejador para códigos HTTP específicos
errorHandler.registerStatusHandler(404, (err, req, res) => {
// Si es una petición a la API
if (req.path.startsWith('/api/')) {
return res.status(404).json({
success: false,
message: 'API endpoint not found'
});
}
// Renderizar página 404 para peticiones web
return res.status(404).render('errors/404', {
message: err.message,
path: req.path
});
});
// Aplicar el manejador de errores al servidor
server.setErrorHandler(errorHandler);Middleware de Error Personalizado
import { NextFunction } from '@foxframework/core';
import { FoxError } from '@foxframework/core';
// Middleware para errores específicos de base de datos
export function databaseErrorMiddleware(err: Error, req: Request, res: Response, next: NextFunction) {
// Solo procesar errores de base de datos
if (err.name === 'SequelizeError' || err.name === 'MongoError') {
// Transformar a FoxError
const foxError = new DatabaseError('Error de base de datos', {
originalError: err,
code: 'DB_ERROR',
details: {
operation: req.method,
entity: req.path.split('/')[2] // Extraer entidad de la URL
}
});
// Log detallado para errores de DB
console.error('[DATABASE ERROR]', {
path: req.path,
method: req.method,
error: err.message,
stack: err.stack,
query: (err as any).sql // Para errores SQL
});
// Continuar con el siguiente manejador de errores
return next(foxError);
}
// Pasar a siguiente manejador si no es un error de DB
return next(err);
}
// Middleware para monitorización de errores
export function monitoringErrorMiddleware(err: Error, req: Request, res: Response, next: NextFunction) {
// Enviar error a sistema de monitorización
if (err instanceof FoxError && err.statusCode >= 500) {
monitoringService.reportError({
message: err.message,
code: err.code,
stack: err.stack,
context: {
url: req.url,
method: req.method,
userId: req.auth?.userId,
requestId: req.id
}
});
}
// Continuar con el siguiente manejador
return next(err);
}
// Registrar en servidor
server.use(databaseErrorMiddleware);
server.use(monitoringErrorMiddleware);
server.useErrorHandler(); // Middleware de error por defecto al finalCasos de Uso Avanzados
Errores de Validación
import { ValidationError } from '@foxframework/core';
import { validateSchema } from '@foxframework/core';
// Definir esquema de validación
const userSchema = {
name: {
type: 'string',
required: true,
minLength: 3,
maxLength: 50
},
email: {
type: 'string',
required: true,
format: 'email'
},
age: {
type: 'number',
min: 18
},
role: {
type: 'string',
enum: ['user', 'admin', 'editor']
}
};
@Post('/users')
async createUser(ctx: HttpContext) {
// Validar datos contra esquema
const { data, errors } = validateSchema(ctx.body, userSchema);
// Lanzar error de validación si hay errores
if (Object.keys(errors).length > 0) {
throw new ValidationError('Datos de usuario inválidos', errors);
}
// Continuar con datos validados
const user = await this.userService.create(data);
return ctx.response.created(user);
}Errores en Operaciones Asíncronas
// Utility para manejar operaciones asíncronas
export const asyncHandler = (fn: (ctx: HttpContext) => Promise<any>) => {
return async (ctx: HttpContext) => {
try {
return await fn(ctx);
} catch (error) {
// Convertir errores no controlados a FoxError
if (!(error instanceof FoxError)) {
error = new InternalServerError('Error en la operación', {
originalError: error
});
}
throw error;
}
};
};
// Uso en controlador
@Controller('/products')
export class ProductController {
@Get('/')
getProducts = asyncHandler(async (ctx: HttpContext) => {
// El error será capturado y transformado automáticamente
const products = await this.productService.findAll();
return products;
});
@Get('/:id')
getProduct = asyncHandler(async (ctx: HttpContext) => {
const { id } = ctx.params;
const product = await this.productService.findById(id);
if (!product) {
throw new NotFoundError(`Producto no encontrado: ${id}`);
}
return product;
});
}Errores con Contexto Enriquecido
export class BusinessError extends FoxError {
constructor(message: string, options?: {
operation?: string;
entity?: string;
transactionId?: string;
source?: string;
statusCode?: number;
code?: string;
details?: Record<string, any>;
}) {
super(message, {
statusCode: options?.statusCode || HttpStatusCode.BAD_REQUEST,
code: options?.code || 'BUSINESS_RULE_VIOLATION',
details: {
operation: options?.operation,
entity: options?.entity,
transactionId: options?.transactionId,
source: options?.source,
...(options?.details || {})
}
});
}
}
// Uso en un servicio
export class OrderService {
async createOrder(data: OrderData): Promise<Order> {
// Verificar reglas de negocio
if (data.items.length === 0) {
throw new BusinessError('No se puede crear una orden sin productos', {
operation: 'createOrder',
entity: 'Order',
details: {
orderData: data
}
});
}
// Verificar stock disponible
for (const item of data.items) {
const product = await this.productRepository.findById(item.productId);
if (!product) {
throw new BusinessError(`Producto no encontrado: ${item.productId}`, {
operation: 'createOrder',
entity: 'Product',
code: 'PRODUCT_NOT_FOUND'
});
}
if (product.stock < item.quantity) {
throw new BusinessError('Stock insuficiente', {
operation: 'createOrder',
entity: 'Product',
code: 'INSUFFICIENT_STOCK',
details: {
productId: product.id,
requested: item.quantity,
available: product.stock
}
});
}
}
// Crear orden...
}
}Formato de Errores Personalizado
// Configuración del formato de respuesta de error
const errorHandler = new ErrorHandler({
errorResponseFormatter: (err, req) => {
const isApiRequest = req.path.startsWith('/api/') ||
req.headers.accept?.includes('application/json');
// Formato para API
if (isApiRequest) {
return {
success: false,
error: {
message: err.message,
code: err.code || 'UNKNOWN_ERROR',
status: err.statusCode,
...(err.details && { details: err.details }),
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
},
timestamp: new Date().toISOString(),
path: req.path,
method: req.method,
requestId: req.id
};
}
// Formato para vistas (HTML)
return {
title: `Error ${err.statusCode}`,
message: err.message,
statusCode: err.statusCode,
stack: process.env.NODE_ENV !== 'production' ? err.stack : undefined,
requestId: req.id
};
}
});
server.setErrorHandler(errorHandler);Internacionalización de Errores
import { FoxError } from '@foxframework/core';
import { i18n } from '@foxframework/core';
export class LocalizedError extends FoxError {
constructor(
messageKey: string,
options?: {
params?: Record<string, any>;
statusCode?: number;
code?: string;
details?: Record<string, any>;
}
) {
super('', {
statusCode: options?.statusCode || HttpStatusCode.BAD_REQUEST,
code: options?.code,
details: options?.details
});
this.messageKey = messageKey;
this.messageParams = options?.params || {};
}
// Claves y parámetros para traducción
private messageKey: string;
private messageParams: Record<string, any>;
// Traducir mensaje cuando se accede
get message(): string {
return i18n.t(this.messageKey, this.messageParams);
}
}
// Middleware para establecer el idioma
const languageMiddleware = (req, res, next) => {
const lang = req.query.lang || req.cookies.lang || req.headers['accept-language'] || 'es';
i18n.setLocale(lang.split(',')[0].slice(0, 2));
next();
};
// Uso en un controlador
@Controller('/users')
export class UserController {
@Post('/')
async createUser(ctx: HttpContext) {
try {
// Validar datos...
if (!ctx.body.email) {
throw new LocalizedError('errors.validation.email_required', {
code: 'VALIDATION_ERROR',
details: { field: 'email' }
});
}
// Verificar si el email ya existe
const existingUser = await this.userService.findByEmail(ctx.body.email);
if (existingUser) {
throw new LocalizedError('errors.users.email_taken', {
code: 'DUPLICATE_EMAIL',
params: { email: ctx.body.email },
statusCode: HttpStatusCode.CONFLICT
});
}
// Crear usuario...
} catch (error) {
throw error;
}
}
}
// Archivos de traducción (locales/es.json)
{
"errors": {
"validation": {
"email_required": "El campo de correo electrónico es obligatorio",
"password_length": "La contraseña debe tener al menos {{min}} caracteres"
},
"users": {
"email_taken": "El correo {{email}} ya está registrado",
"not_found": "Usuario no encontrado con ID: {{id}}"
}
}
}Integración con Servicios Externos
Captura de Errores en Servicios Externos
import axios from 'axios';
import { ServiceUnavailableError, BadGatewayError } from '@foxframework/core';
export class PaymentGatewayService {
async processPayment(paymentData: PaymentData): Promise<PaymentResult> {
try {
// Intentar procesar el pago con API externa
const response = await axios.post(
'https://payment-gateway.com/api/process',
paymentData,
{ timeout: 5000 }
);
return response.data;
} catch (error) {
// Transformar errores de Axios a errores de Fox Framework
// Error de timeout
if (error.code === 'ECONNABORTED') {
throw new ServiceUnavailableError('El servicio de pagos no responde', {
code: 'PAYMENT_TIMEOUT',
details: {
paymentProvider: 'PaymentGateway',
operation: 'processPayment'
}
});
}
// Error de red
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
throw new ServiceUnavailableError('No se puede conectar al servicio de pagos', {
code: 'PAYMENT_CONNECTION_ERROR',
originalError: error
});
}
// Error de respuesta con datos
if (error.response) {
const status = error.response.status;
const data = error.response.data;
// Mapeo de errores específicos del gateway
if (status === 400) {
throw new BadRequestError(data.message || 'Datos de pago inválidos', {
code: `PAYMENT_ERROR_${data.errorCode || 'UNKNOWN'}`,
details: data
});
}
if (status === 401 || status === 403) {
throw new BadGatewayError('Error de autenticación con el servicio de pagos', {
code: 'PAYMENT_AUTH_ERROR',
details: data
});
}
// Error genérico con respuesta
throw new BadGatewayError(
data.message || 'Error en el procesamiento del pago',
{
code: `PAYMENT_ERROR_${status}`,
details: data
}
);
}
// Error desconocido
throw new ServiceUnavailableError('Error en el servicio de pagos', {
code: 'PAYMENT_UNKNOWN_ERROR',
originalError: error
});
}
}
}Debugging y Diagnóstico
Mejora de Stack Traces
import { FoxError } from '@foxframework/core';
import { StackTraceEnhancer } from '@foxframework/core';
// Configurar el mejorador de stack traces
const stackTraceEnhancer = new StackTraceEnhancer({
// Directorio base de la aplicación para relativizar rutas
appRoot: process.cwd(),
// Número de líneas de contexto a mostrar
contextLines: 3,
// Filtrar archivos del framework
filterNodeModules: true,
// Incluir variables locales cuando sea posible
includeLocals: process.env.NODE_ENV !== 'production'
});
// Extender FoxError para usar stack traces mejorados
class EnhancedFoxError extends FoxError {
constructor(message: string, options?: FoxErrorOptions) {
super(message, options);
// Mejorar stack trace en modo desarrollo
if (process.env.NODE_ENV !== 'production') {
this.stack = stackTraceEnhancer.enhance(this);
}
}
}
// Usar en el sistema
export class ValidationError extends EnhancedFoxError {
// Implementación como antes
}Error Aggregator para Problemas Recurrentes
import { ErrorAggregator } from '@foxframework/core';
// Crear un agregador de errores
const errorAggregator = new ErrorAggregator({
// Tiempo para considerar errores como relacionados
timeWindow: 5 * 60 * 1000, // 5 minutos
// Umbral de errores similares para generar alerta
threshold: 5,
// Función para determinar si dos errores son similares
similarityChecker: (err1, err2) => {
// Considerar similares si tienen el mismo código o mensaje
return (
err1.code === err2.code ||
err1.message === err2.message ||
(err1.originalError?.name === err2.originalError?.name)
);
},
// Acción a tomar cuando se detectan errores recurrentes
onThresholdReached: (errors) => {
console.error(`⚠️ Se han detectado ${errors.length} errores similares en los últimos 5 minutos:`, {
code: errors[0].code,
message: errors[0].message,
occurrences: errors.length,
firstOccurrence: errors[0].timestamp,
lastOccurrence: errors[errors.length - 1].timestamp
});
// Notificar al sistema de monitorización
monitoringService.sendAlert({
type: 'ERROR_SPIKE',
message: `Spike de errores detectado: ${errors[0].message}`,
count: errors.length,
samples: errors.slice(0, 3)
});
}
});
// Integrarlo con el sistema de errores
errorHandler.onError((err, req) => {
// Registrar en el agregador
errorAggregator.add({
code: err.code,
message: err.message,
statusCode: err.statusCode,
path: req.path,
method: req.method,
timestamp: new Date(),
originalError: err.originalError
});
});Testing
import { expect } from 'chai';
import { FoxError, NotFoundError } from '@foxframework/core';
describe('Error Handler', () => {
let server;
beforeEach(() => {
// Crear servidor para testing
server = FoxFactory.createServer({
port: 3000,
errorHandler: {
exposeErrors: true // Mostrar detalles completos en tests
}
});
// Configurar rutas para testing
server.router.get('/test/not-found', () => {
throw new NotFoundError('Recurso de prueba no encontrado');
});
server.router.get('/test/custom-error', () => {
throw new FoxError('Error personalizado', {
statusCode: 418, // I'm a teapot
code: 'TEAPOT_ERROR',
details: { reason: 'Just testing' }
});
});
server.router.get('/test/async-error', async () => {
// Simular error asíncrono
await Promise.resolve();
throw new Error('Error asíncrono no controlado');
});
});
it('should handle NotFoundError correctly', async () => {
// Realizar petición
const response = await request(server.app)
.get('/test/not-found')
.set('Accept', 'application/json');
// Verificar respuesta
expect(response.status).to.equal(404);
expect(response.body).to.have.property('message', 'Recurso de prueba no encontrado');
expect(response.body).to.have.property('code', 'NOT_FOUND');
});
it('should handle custom error with custom status code', async () => {
const response = await request(server.app)
.get('/test/custom-error')
.set('Accept', 'application/json');
expect(response.status).to.equal(418);
expect(response.body).to.have.property('code', 'TEAPOT_ERROR');
expect(response.body).to.have.property('details');
expect(response.body.details).to.have.property('reason', 'Just testing');
});
it('should convert unhandled errors to InternalServerError', async () => {
const response = await request(server.app)
.get('/test/async-error')
.set('Accept', 'application/json');
expect(response.status).to.equal(500);
expect(response.body).to.have.property('code', 'INTERNAL_SERVER_ERROR');
// En modo test, debería incluir el stack
expect(response.body).to.have.property('stack');
});
it('should render HTML error page for browser requests', async () => {
const response = await request(server.app)
.get('/test/not-found')
.set('Accept', 'text/html');
expect(response.status).to.equal(404);
expect(response.type).to.equal('text/html');
expect(response.text).to.include('Recurso de prueba no encontrado');
});
});Buenas Prácticas
Jerarquía de Errores
Organiza tus errores en una jerarquía que refleje tus dominios de negocio:
// Error base del framework
export class FoxError extends Error { /* ... */ }
// Errores HTTP generales
export class BadRequestError extends FoxError { /* ... */ }
export class UnauthorizedError extends FoxError { /* ... */ }
// ...otros errores HTTP
// Errores de dominio
export class DomainError extends FoxError { /* ... */ }
// Errores específicos de autenticación
export class AuthError extends DomainError { /* ... */ }
export class InvalidCredentialsError extends AuthError { /* ... */ }
export class AccountLockedError extends AuthError { /* ... */ }
// Errores de negocio
export class BusinessError extends DomainError { /* ... */ }
export class InsufficientFundsError extends BusinessError { /* ... */ }
export class ProductOutOfStockError extends BusinessError { /* ... */ }
// Errores de infraestructura
export class InfrastructureError extends FoxError { /* ... */ }
export class DatabaseError extends InfrastructureError { /* ... */ }
export class CacheError extends InfrastructureError { /* ... */ }
export class NetworkError extends InfrastructureError { /* ... */ }
// Errores de integración
export class IntegrationError extends InfrastructureError { /* ... */ }
export class ApiError extends IntegrationError { /* ... */ }
export class WebhookError extends IntegrationError { /* ... */ }Captura Selectiva de Errores
try {
// Operación que puede fallar
await riskyOperation();
} catch (error) {
// Capturar solo errores específicos
if (error instanceof DatabaseError) {
// Manejar error de base de datos
logger.error('Error de base de datos', { error });
throw new ServiceUnavailableError(
'El servicio no está disponible en este momento',
{ originalError: error }
);
} else if (error instanceof ValidationError) {
// Re-lanzar errores de validación directamente
throw error;
} else if (error instanceof BusinessError) {
// Transformar errores de negocio para la capa de presentación
throw new BadRequestError(error.message, {
code: error.code,
details: error.details
});
} else {
// Error desconocido
logger.error('Error inesperado', { error });
throw new InternalServerError('Ha ocurrido un error inesperado', {
originalError: error
});
}
}Evitar Filtración de Información Sensible
// Función para sanitizar errores antes de enviarlos al cliente
function sanitizeErrorForResponse(error: any, isProduction: boolean): Record<string, any> {
// Clonar para no modificar el original
const sanitized: Record<string, any> = {
message: error.message,
code: error.code || 'UNKNOWN_ERROR',
statusCode: error.statusCode || 500
};
// En producción, ocultar detalles internos
if (isProduction) {
// Mensajes genéricos para errores de servidor
if (sanitized.statusCode >= 500) {
sanitized.message = 'Error interno del servidor';
}
// Evitar exponer detalles de errores de infraestructura
if (error instanceof InfrastructureError) {
delete sanitized.details;
}
// Eliminar información sensible
const sensitiveFields = ['password', 'token', 'secret', 'key', 'credential'];
if (sanitized.details) {
for (const field of sensitiveFields) {
if (field in sanitized.details) {
sanitized.details[field] = '[REDACTED]';
}
}
}
// Nunca incluir stack trace en producción
delete sanitized.stack;
} else {
// En desarrollo, incluir stack y detalles
if (error.stack) {
sanitized.stack = error.stack;
}
if (error.details) {
sanitized.details = error.details;
}
}
return sanitized;
}
// Usar en el handler de errores
errorHandler.onError((err, req, res) => {
const isProduction = process.env.NODE_ENV === 'production';
const sanitizedError = sanitizeErrorForResponse(err, isProduction);
res.status(err.statusCode || 500).json(sanitizedError);
});Registrar Errores de Forma Efectiva
// Configuración del logger para errores
const errorLogger = {
log: (err: FoxError, req: Request) => {
// Determinar nivel de log según el tipo de error
let level = 'error';
if (err.statusCode < 400) level = 'info';
else if (err.statusCode < 500) level = 'warn';
else level = 'error';
// Base del mensaje de log
const logData = {
statusCode: err.statusCode,
code: err.code,
message: err.message,
path: req.path,
method: req.method,
requestId: req.id,
userId: req.auth?.userId,
timestamp: new Date().toISOString()
};
// Añadir detalles si están disponibles
if (err.details) {
logData.details = err.details;
}
// Añadir stack trace para errores de servidor
if (err.statusCode >= 500) {
logData.stack = err.stack;
// Incluir error original si existe
if (err.originalError) {
logData.originalError = {
message: err.originalError.message,
name: err.originalError.name,
stack: err.originalError.stack
};
}
}
// Log según nivel
console[level](`[${level.toUpperCase()}] Error handled:`, logData);
}
};
// Registrar en manejador de errores
errorHandler.setLogger(errorLogger);Conclusión
El sistema de manejo de errores de Fox Framework proporciona una forma robusta y flexible para capturar, gestionar y responder a errores en tus aplicaciones. Con su jerarquía de errores tipados, capacidades de personalización y herramientas de diagnóstico, te permite crear aplicaciones resilientes que manejan fallos de forma elegante tanto en desarrollo como en producción.
Al utilizar este sistema de forma efectiva, podrás:
- Proporcionar experiencias de usuario coherentes incluso cuando ocurren errores
- Depurar problemas más rápidamente con información contextual enriquecida
- Implementar estrategias específicas de manejo de errores para diferentes situaciones
- Mantener un registro detallado de errores para análisis y monitorización
- Asegurar que las respuestas de error son seguras y apropiadas para cada contexto
Recuerda que un buen manejo de errores no sólo mejora la experiencia del usuario, sino que también facilita enormemente el mantenimiento y la evolución de tu aplicación a lo largo del tiempo.