Manejo de Errores Profesional
Publicado el 14 de octubre de 2025
Manejo de Errores Profesional
El manejo de errores es una de las partes más importantes del desarrollo de software, y también una de las más ignoradas. He visto aplicaciones que funcionan perfectamente en el “happy path”, pero que colapsan de manera inesperada cuando algo sale mal. He visto código donde la lógica de negocio está mezclada con el manejo de errores, haciendo que ambos sean difíciles de entender.
El manejo de errores profesional no es solo sobre capturar excepciones. Es sobre comunicar claramente qué salió mal, por qué salió mal, y qué puede hacer el código que llama para manejarlo. Es sobre separar la lógica de negocio del manejo de errores, y sobre usar patrones que hagan el código más robusto y mantenible.
En este artículo, compartiré por qué retornar null es problemático, cómo usar Result Patterns, y cómo gestionar errores de manera profesional sin ensuciar tu lógica de negocio.
El problema con retornar null
Retornar null cuando algo falla es una práctica común, pero problemática. null no comunica información sobre qué salió mal o por qué.
❌ Mal: Retornar null
function getUser(id) {
const user = database.findById(id);
if (!user) {
return null; // ¿Por qué es null? ¿No existe? ¿Error de DB?
}
return user;
}
function processUser(userId) {
const user = getUser(userId);
// ¿Qué pasa si user es null?
// ¿Es un error? ¿Debo continuar?
console.log(user.name); // 💥 Error si user es null
}
Problemas:
nullno comunica qué salió mal- Es fácil olvidar verificar
null - No hay distinción entre “no encontrado” y “error de base de datos”
- El código que llama debe adivinar qué significa
null
✅ Bien: Lanzar excepciones específicas
class UserNotFoundError extends Error {
constructor(userId) {
super(`User with id ${userId} not found`);
this.name = 'UserNotFoundError';
this.userId = userId;
}
}
class DatabaseError extends Error {
constructor(originalError) {
super('Database operation failed');
this.name = 'DatabaseError';
this.originalError = originalError;
}
}
function getUser(id) {
try {
const user = database.findById(id);
if (!user) {
throw new UserNotFoundError(id);
}
return user;
} catch (error) {
if (error instanceof UserNotFoundError) {
throw error; // Re-lanzar errores específicos
}
throw new DatabaseError(error);
}
}
function processUser(userId) {
try {
const user = getUser(userId);
console.log(user.name); // Seguro: user nunca es null
} catch (error) {
if (error instanceof UserNotFoundError) {
// Manejar usuario no encontrado
console.error('User not found:', error.userId);
} else if (error instanceof DatabaseError) {
// Manejar error de base de datos
console.error('Database error:', error.originalError);
} else {
// Error inesperado
throw error;
}
}
}
Ahora el código comunica claramente qué salió mal y por qué.
Result Pattern: Una alternativa a las excepciones
El Result Pattern es una alternativa funcional a las excepciones. En lugar de lanzar excepciones, retornas un objeto Result que contiene el valor exitoso o el error.
Implementación básica de Result
class Result {
constructor(isSuccess, value, error) {
this.isSuccess = isSuccess;
this.isFailure = !isSuccess;
this.value = value;
this.error = error;
}
static success(value) {
return new Result(true, value, null);
}
static failure(error) {
return new Result(false, null, error);
}
map(fn) {
if (this.isFailure) {
return this;
}
try {
return Result.success(fn(this.value));
} catch (error) {
return Result.failure(error);
}
}
flatMap(fn) {
if (this.isFailure) {
return this;
}
return fn(this.value);
}
onSuccess(fn) {
if (this.isSuccess) {
fn(this.value);
}
return this;
}
onFailure(fn) {
if (this.isFailure) {
fn(this.error);
}
return this;
}
getOrElse(defaultValue) {
return this.isSuccess ? this.value : defaultValue;
}
getOrThrow() {
if (this.isFailure) {
throw this.error;
}
return this.value;
}
}
Uso del Result Pattern
function getUser(id) {
try {
const user = database.findById(id);
if (!user) {
return Result.failure(new UserNotFoundError(id));
}
return Result.success(user);
} catch (error) {
return Result.failure(new DatabaseError(error));
}
}
function processUser(userId) {
return getUser(userId)
.onSuccess(user => {
console.log('User found:', user.name);
})
.onFailure(error => {
if (error instanceof UserNotFoundError) {
console.error('User not found:', error.userId);
} else {
console.error('Error:', error);
}
});
}
// O usando map/flatMap para transformaciones
function getUserEmail(userId) {
return getUser(userId)
.map(user => user.email)
.getOrElse('unknown@example.com');
}
Ventajas del Result Pattern
- Explícito: El tipo de retorno comunica que puede fallar
- Composable: Puedes encadenar operaciones fácilmente
- Sin excepciones: No necesitas try/catch en cada llamada
- Forzado a manejar: El código que llama debe manejar el error explícitamente
Desventajas del Result Pattern
- Más verboso: Cada función debe retornar
Result - No estándar: No todos los lenguajes lo soportan nativamente
- Curva de aprendizaje: Requiere entender programación funcional
Separando lógica de negocio del manejo de errores
Una de las claves del manejo de errores profesional es separar la lógica de negocio del manejo de errores.
❌ Mal: Lógica mezclada con manejo de errores
function processOrder(order) {
try {
// Validar pedido
if (!order.items || order.items.length === 0) {
throw new Error('Order has no items');
}
// Calcular total
let total = 0;
for (let item of order.items) {
if (!item.price || item.price < 0) {
throw new Error('Invalid item price');
}
total += item.price * item.quantity;
}
// Guardar en base de datos
try {
database.save(order);
} catch (error) {
console.error('Database error:', error);
throw new Error('Failed to save order');
}
// Enviar email
try {
emailService.send(order.customerEmail, 'Order confirmed');
} catch (error) {
console.error('Email error:', error);
// ¿Qué hacer? ¿Reversar el pedido?
}
return total;
} catch (error) {
console.error('Error processing order:', error);
throw error;
}
}
Problemas:
- La lógica de negocio está mezclada con el manejo de errores
- Difícil de entender qué hace la función
- Difícil de probar
- El manejo de errores está disperso
✅ Bien: Lógica separada del manejo de errores
// Lógica de negocio pura
class OrderProcessor {
validateOrder(order) {
if (!order.items || order.items.length === 0) {
throw new ValidationError('Order must have at least one item');
}
for (let item of order.items) {
if (!item.price || item.price < 0) {
throw new ValidationError(`Invalid price for item ${item.id}`);
}
}
}
calculateTotal(order) {
return order.items.reduce(
(total, item) => total + (item.price * item.quantity),
0
);
}
async saveOrder(order) {
return await orderRepository.save(order);
}
async sendConfirmationEmail(order) {
return await emailService.send(
order.customerEmail,
'Order confirmed',
`Your order total is $${order.total}`
);
}
}
// Manejo de errores en una capa superior
class OrderService {
constructor(orderProcessor, orderRepository, emailService) {
this.processor = orderProcessor;
this.repository = orderRepository;
this.emailService = emailService;
}
async processOrder(orderData) {
try {
// Lógica de negocio (sin manejo de errores)
this.processor.validateOrder(orderData);
const total = this.processor.calculateTotal(orderData);
const order = { ...orderData, total };
await this.processor.saveOrder(order);
// Email no crítico: no falla si no se envía
try {
await this.processor.sendConfirmationEmail(order);
} catch (emailError) {
logger.warn('Failed to send confirmation email', { order, emailError });
// Continuar: el pedido ya está guardado
}
return Result.success(order);
} catch (error) {
// Manejo centralizado de errores
return this.handleError(error, orderData);
}
}
handleError(error, orderData) {
if (error instanceof ValidationError) {
logger.warn('Validation error', { error, orderData });
return Result.failure(new InvalidOrderError(error.message));
} else if (error instanceof DatabaseError) {
logger.error('Database error', { error, orderData });
return Result.failure(new OrderProcessingError('Failed to save order'));
} else {
logger.error('Unexpected error', { error, orderData });
return Result.failure(new UnexpectedError(error));
}
}
}
Ahora:
- La lógica de negocio es clara y testeable
- El manejo de errores está centralizado
- Es fácil entender qué hace cada parte
- Es fácil agregar nuevos tipos de errores
Tipos de errores y cuándo usarlos
Errores de validación
Errores que indican que los datos de entrada son inválidos. El cliente puede corregirlos.
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
// Uso
if (!email || !email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
Errores de dominio
Errores que violan reglas de negocio. El cliente puede corregirlos.
class InsufficientFundsError extends Error {
constructor(required, available) {
super(`Insufficient funds. Required: ${required}, Available: ${available}`);
this.name = 'InsufficientFundsError';
this.required = required;
this.available = available;
}
}
// Uso
if (account.balance < amount) {
throw new InsufficientFundsError(amount, account.balance);
}
Errores de infraestructura
Errores técnicos que el cliente no puede corregir. Requieren acción del equipo.
class DatabaseError extends Error {
constructor(originalError) {
super('Database operation failed');
this.name = 'DatabaseError';
this.originalError = originalError;
}
}
// Uso
try {
await database.save(data);
} catch (error) {
throw new DatabaseError(error);
}
Errores inesperados
Errores que no deberían ocurrir. Indican bugs o problemas del sistema.
class UnexpectedError extends Error {
constructor(originalError) {
super('An unexpected error occurred');
this.name = 'UnexpectedError';
this.originalError = originalError;
}
}
Estrategias de manejo de errores
1. Fail Fast
Detecta errores tan pronto como sea posible.
function processPayment(amount, account) {
// Validar temprano
if (amount <= 0) {
throw new ValidationError('Amount must be positive');
}
if (!account || !account.isActive) {
throw new ValidationError('Account must be active');
}
// Continuar con la lógica
// ...
}
2. Retry con backoff exponencial
Para errores transitorios (red, base de datos temporalmente no disponible).
async function retryWithBackoff(fn, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (!isTransientError(error) || attempt === maxRetries - 1) {
throw error;
}
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await sleep(delay);
}
}
}
async function saveWithRetry(data) {
return await retryWithBackoff(() => database.save(data));
}
3. Circuit Breaker
Previene que un servicio fallido sobrecargue el sistema.
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new CircuitBreakerOpenError('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
Logging y monitoreo
El manejo de errores profesional incluye logging y monitoreo adecuados.
❌ Mal: Logging genérico
try {
await processOrder(order);
} catch (error) {
console.error('Error:', error); // No suficiente información
}
✅ Bien: Logging estructurado
try {
await processOrder(order);
} catch (error) {
logger.error('Failed to process order', {
error: error.message,
stack: error.stack,
orderId: order.id,
userId: order.userId,
amount: order.total,
timestamp: new Date().toISOString(),
context: {
service: 'OrderService',
method: 'processOrder',
environment: process.env.NODE_ENV
}
});
// También enviar a servicio de monitoreo
monitoringService.recordError('order_processing_failed', {
errorType: error.constructor.name,
orderId: order.id
});
throw error;
}
Mi experiencia práctica
He visto proyectos donde el manejo de errores era un afterthought. Errores genéricos, null retornado en todas partes, lógica de negocio mezclada con manejo de errores. Cada bug era difícil de encontrar y arreglar.
He visto proyectos con manejo de errores profesional. Errores específicos, Result Patterns donde tenía sentido, lógica separada del manejo de errores. Los bugs eran fáciles de encontrar y arreglar.
En la práctica, puedes usar una combinación de excepciones específicas y Result Patterns. Para operaciones críticas, usa excepciones. Para operaciones que pueden fallar frecuentemente, usa Result Patterns.
Para aplicaciones donde la precisión es crítica (finanzas, salud), usa excepciones específicas para cada tipo de error, con logging detallado y monitoreo.
Mi perspectiva personal
El manejo de errores profesional no es solo sobre capturar excepciones. Es sobre:
- Comunicar claramente: Los errores deben decir qué salió mal y por qué
- Separar responsabilidades: La lógica de negocio no debe estar mezclada con el manejo de errores
- Usar patrones apropiados: Excepciones para errores excepcionales, Result Patterns para operaciones que pueden fallar
- Logging y monitoreo: Los errores deben ser registrados y monitoreados
He visto proyectos donde el manejo de errores era un afterthought. Errores genéricos, null retornado en todas partes, lógica de negocio mezclada con manejo de errores. Cada bug era difícil de encontrar y arreglar.
He visto proyectos con manejo de errores profesional. Errores específicos, Result Patterns donde tenía sentido, lógica separada del manejo de errores. Los bugs eran fáciles de encontrar y arreglar.
No retornes null. No uses excepciones genéricas. No mezcles lógica de negocio con manejo de errores.
Escribe código que comunique claramente qué puede salir mal, y que maneje esos casos de manera profesional. Tu código futuro (y tu equipo) te lo agradecerán.