Mira la lección en vídeo
Accede al vídeo completo de esta lección y a más contenido exclusivo con el Plan Plus.
Desbloquear Plan PlusDiseño de estrategias robustas para el control de errores: Patrones y mejores prácticas
El manejo efectivo de errores va más allá de simplemente capturar y lanzar excepciones. Requiere un enfoque estructurado y sistemático que permita crear aplicaciones resilientes capaces de recuperarse de situaciones inesperadas. En esta sección exploraremos patrones y mejores prácticas para implementar estrategias robustas de control de errores en JavaScript.
Jerarquía personalizada de errores
Crear una jerarquía de errores personalizada permite categorizar los problemas de forma más precisa y facilita su manejo específico:
// Clase base para errores de la aplicación
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
// Captura la pila de llamadas para mejor depuración
Error.captureStackTrace(this, this.constructor);
}
}
// Errores específicos que extienden la clase base
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
this.validationErrors = [];
}
addValidationError(field, message) {
this.validationErrors.push({ field, message });
return this;
}
}
class NotFoundError extends AppError {
constructor(entity, query) {
super(`${entity} not found with ${query}`, 404);
this.entity = entity;
this.query = query;
}
}
Esta estructura permite identificar rápidamente el tipo de error y proporciona información contextual adicional para facilitar la depuración.
Patrón de manejo centralizado de errores
Implementar un manejador centralizado de errores evita la duplicación de código y garantiza un tratamiento consistente:
// Función para manejar errores de forma centralizada
function handleError(error, context = {}) {
// Enriquece el error con información contextual
const enrichedError = {
message: error.message,
name: error.name,
stack: error.stack,
timestamp: new Date().toISOString(),
context
};
// Comportamiento específico según el tipo de error
if (error instanceof ValidationError) {
console.warn('Validation failed:', enrichedError);
// Lógica para errores de validación
} else if (error instanceof NotFoundError) {
console.info('Resource not found:', enrichedError);
// Lógica para recursos no encontrados
} else {
console.error('Unexpected error:', enrichedError);
// Lógica para errores inesperados
}
// Posible integración con sistemas de monitoreo
// reportErrorToMonitoringService(enrichedError);
return enrichedError;
}
Este enfoque permite escalar el manejo de errores y facilita la integración con servicios de monitoreo externos.
Patrón Try-Catch-Finally con limpieza de recursos
Es fundamental asegurar que los recursos se liberen adecuadamente incluso cuando ocurren errores:
async function processFile(filePath) {
let fileHandle = null;
try {
// Adquirir recursos
fileHandle = await fs.promises.open(filePath, 'r');
const content = await fileHandle.readFile('utf8');
return processContent(content);
} catch (error) {
// Manejar el error específicamente
if (error.code === 'ENOENT') {
throw new NotFoundError('File', filePath);
}
throw error; // Re-lanzar otros errores
} finally {
// Garantizar la liberación de recursos
if (fileHandle) {
await fileHandle.close().catch(err => {
console.error('Error closing file:', err);
});
}
}
}
El bloque finally
garantiza que la limpieza de recursos ocurra independientemente del resultado de la operación.
Errores específicos del dominio
Diseñar errores que reflejen el dominio de la aplicación mejora la legibilidad y el mantenimiento:
class InsufficientFundsError extends AppError {
constructor(accountId, amount, balance) {
super(`Account ${accountId} has insufficient funds for transaction of ${amount}`, 400);
this.accountId = accountId;
this.amount = amount;
this.balance = balance;
this.shortfall = amount - balance;
}
}
// Uso en el contexto de negocio
function withdrawMoney(accountId, amount) {
const account = findAccount(accountId);
if (account.balance < amount) {
throw new InsufficientFundsError(accountId, amount, account.balance);
}
// Proceder con la transacción
account.balance -= amount;
return account;
}
Estos errores proporcionan información contextual valiosa que puede utilizarse para tomar decisiones de recuperación o para informar al usuario.
Patrón de envoltorio (Wrapper) para manejo de errores
Encapsular operaciones propensas a errores en funciones de orden superior simplifica el código y reduce la duplicación:
// Función envoltorio para operaciones asíncronas
const withErrorHandling = (fn, errorHandler) => async (...args) => {
try {
return await fn(...args);
} catch (error) {
return errorHandler(error, { functionName: fn.name, arguments: args });
}
};
// Uso del envoltorio
const safeGetUser = withErrorHandling(
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.statusText}`);
}
return response.json();
},
(error, context) => {
console.error(`Error fetching user: ${error.message}`, context);
return { error: true, message: 'Unable to retrieve user information' };
}
);
// Uso simplificado
const user = await safeGetUser(123);
if (user.error) {
// Manejar el caso de error
}
Este patrón separa la lógica de negocio del manejo de errores, mejorando la legibilidad y mantenibilidad.
Fail-fast vs. Fail-safe
Elegir entre enfoques fail-fast (fallar rápido) y fail-safe (fallar de forma segura) depende del contexto:
// Enfoque fail-fast: detiene la ejecución ante el primer error
function validateUserDataStrict(userData) {
if (!userData.name) throw new ValidationError('Name is required');
if (!userData.email) throw new ValidationError('Email is required');
if (!isValidEmail(userData.email)) throw new ValidationError('Email is invalid');
return true; // Todos los datos son válidos
}
// Enfoque fail-safe: recopila todos los errores antes de fallar
function validateUserDataCollective(userData) {
const error = new ValidationError('Validation failed');
if (!userData.name) error.addValidationError('name', 'Name is required');
if (!userData.email) error.addValidationError('email', 'Email is required');
else if (!isValidEmail(userData.email)) error.addValidationError('email', 'Email is invalid');
if (error.validationErrors.length > 0) {
throw error;
}
return true; // Todos los datos son válidos
}
El enfoque fail-fast es útil durante el desarrollo para detectar problemas rápidamente, mientras que el enfoque fail-safe proporciona mejor experiencia de usuario en producción.
Estrategia de reintentos con retroceso exponencial
Para operaciones que pueden fallar temporalmente, implementar una estrategia de reintentos puede aumentar la resiliencia:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
const { retryDelay = 300, retryMultiplier = 2 } = options;
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fetch(url, options);
} catch (error) {
lastError = error;
// Verificar si el error es recuperable
if (!isRetryableError(error)) {
throw error;
}
// Calcular retraso con retroceso exponencial
const delay = retryDelay * Math.pow(retryMultiplier, attempt);
console.warn(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`);
// Esperar antes del siguiente intento
await new Promise(resolve => setTimeout(resolve, delay));
}
}
// Si llegamos aquí, todos los intentos fallaron
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
// Función auxiliar para determinar si un error permite reintento
function isRetryableError(error) {
// Errores de red o errores de servidor (5xx) suelen ser recuperables
return error.name === 'NetworkError' ||
(error.response && error.response.status >= 500);
}
Esta estrategia es especialmente útil para operaciones de red o interacciones con servicios externos que pueden experimentar fallos transitorios.
Registro y monitoreo de errores
Un sistema robusto de manejo de errores debe incluir registro detallado para facilitar la depuración y el análisis:
function logError(error, severity = 'error', metadata = {}) {
const logEntry = {
message: error.message,
name: error.name,
stack: error.stack,
severity,
timestamp: new Date().toISOString(),
...metadata
};
// Registro local para desarrollo
console[severity](JSON.stringify(logEntry, null, 2));
// En producción, enviar a un servicio de registro
if (process.env.NODE_ENV === 'production') {
sendToLoggingService(logEntry);
}
return logEntry;
}
// Ejemplo de uso con contexto adicional
try {
processPayment(order);
} catch (error) {
logError(error, 'error', {
orderId: order.id,
userId: order.userId,
amount: order.total
});
// Manejo específico según el tipo de error
if (error instanceof PaymentGatewayError) {
notifyAdminOfPaymentIssue(error, order);
}
// Informar al usuario de manera amigable
return { success: false, message: 'Unable to process payment at this time' };
}
Un buen sistema de registro proporciona visibilidad sobre los problemas en producción y facilita la identificación de patrones de error.
Degradación elegante
Diseñar sistemas que puedan degradarse elegantemente cuando ocurren errores mejora la experiencia del usuario:
async function loadDashboardData(userId) {
const results = {
user: null,
recentActivity: [],
recommendations: [],
notifications: []
};
// Cargar datos en paralelo con manejo individual de errores
await Promise.allSettled([
// Datos críticos - si fallan, la página no es útil
fetchUserProfile(userId)
.then(data => { results.user = data; })
.catch(error => {
logError(error, 'error', { component: 'userProfile', userId });
throw error; // Re-lanzar errores críticos
}),
// Datos no críticos - la página sigue siendo útil sin ellos
fetchRecentActivity(userId)
.then(data => { results.recentActivity = data; })
.catch(error => {
logError(error, 'warn', { component: 'recentActivity', userId });
// No re-lanzar, usar datos por defecto
}),
fetchRecommendations(userId)
.then(data => { results.recommendations = data; })
.catch(error => {
logError(error, 'warn', { component: 'recommendations', userId });
// No re-lanzar, usar datos por defecto
}),
fetchNotifications(userId)
.then(data => { results.notifications = data; })
.catch(error => {
logError(error, 'warn', { component: 'notifications', userId });
// No re-lanzar, usar datos por defecto
})
]);
// Verificar si los datos críticos se cargaron correctamente
if (!results.user) {
throw new Error('Failed to load critical user data');
}
return results;
}
Este enfoque permite que la aplicación siga funcionando parcialmente incluso cuando algunos componentes fallan, priorizando la disponibilidad de las funcionalidades críticas.
Diseño de estrategias robustas para el control de errores: Patrones y mejores prácticas
Guarda tu progreso
Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.
Más de 25.000 desarrolladores ya confían en CertiDevs
El manejo efectivo de errores va más allá de simplemente capturar y lanzar excepciones. Requiere un enfoque estructurado y sistemático que permita crear aplicaciones resilientes capaces de recuperarse de situaciones inesperadas. En esta sección exploraremos patrones y mejores prácticas para implementar estrategias robustas de control de errores en JavaScript.
Jerarquía personalizada de errores
Crear una jerarquía de errores personalizada permite categorizar los problemas de forma más precisa y facilita su manejo específico:
// Clase base para errores de la aplicación
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
// Captura la pila de llamadas para mejor depuración
Error.captureStackTrace(this, this.constructor);
}
}
// Errores específicos que extienden la clase base
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
this.validationErrors = [];
}
addValidationError(field, message) {
this.validationErrors.push({ field, message });
return this;
}
}
class NotFoundError extends AppError {
constructor(entity, query) {
super(`${entity} not found with ${query}`, 404);
this.entity = entity;
this.query = query;
}
}
Esta estructura permite identificar rápidamente el tipo de error y proporciona información contextual adicional para facilitar la depuración.
Patrón de manejo centralizado de errores
Implementar un manejador centralizado de errores evita la duplicación de código y garantiza un tratamiento consistente:
// Función para manejar errores de forma centralizada
function handleError(error, context = {}) {
// Enriquece el error con información contextual
const enrichedError = {
message: error.message,
name: error.name,
stack: error.stack,
timestamp: new Date().toISOString(),
context
};
// Comportamiento específico según el tipo de error
if (error instanceof ValidationError) {
console.warn('Validation failed:', enrichedError);
// Lógica para errores de validación
} else if (error instanceof NotFoundError) {
console.info('Resource not found:', enrichedError);
// Lógica para recursos no encontrados
} else {
console.error('Unexpected error:', enrichedError);
// Lógica para errores inesperados
}
// Posible integración con sistemas de monitoreo
// reportErrorToMonitoringService(enrichedError);
return enrichedError;
}
Este enfoque permite escalar el manejo de errores y facilita la integración con servicios de monitoreo externos.
Patrón Try-Catch-Finally con limpieza de recursos
Es fundamental asegurar que los recursos se liberen adecuadamente incluso cuando ocurren errores:
async function processFile(filePath) {
let fileHandle = null;
try {
// Adquirir recursos
fileHandle = await fs.promises.open(filePath, 'r');
const content = await fileHandle.readFile('utf8');
return processContent(content);
} catch (error) {
// Manejar el error específicamente
if (error.code === 'ENOENT') {
throw new NotFoundError('File', filePath);
}
throw error; // Re-lanzar otros errores
} finally {
// Garantizar la liberación de recursos
if (fileHandle) {
await fileHandle.close().catch(err => {
console.error('Error closing file:', err);
});
}
}
}
El bloque finally
garantiza que la limpieza de recursos ocurra independientemente del resultado de la operación.
Errores específicos del dominio
Diseñar errores que reflejen el dominio de la aplicación mejora la legibilidad y el mantenimiento:
class InsufficientFundsError extends AppError {
constructor(accountId, amount, balance) {
super(`Account ${accountId} has insufficient funds for transaction of ${amount}`, 400);
this.accountId = accountId;
this.amount = amount;
this.balance = balance;
this.shortfall = amount - balance;
}
}
// Uso en el contexto de negocio
function withdrawMoney(accountId, amount) {
const account = findAccount(accountId);
if (account.balance < amount) {
throw new InsufficientFundsError(accountId, amount, account.balance);
}
// Proceder con la transacción
account.balance -= amount;
return account;
}
Estos errores proporcionan información contextual valiosa que puede utilizarse para tomar decisiones de recuperación o para informar al usuario.
Patrón de envoltorio (Wrapper) para manejo de errores
Encapsular operaciones propensas a errores en funciones de orden superior simplifica el código y reduce la duplicación:
// Función envoltorio para operaciones asíncronas
const withErrorHandling = (fn, errorHandler) => async (...args) => {
try {
return await fn(...args);
} catch (error) {
return errorHandler(error, { functionName: fn.name, arguments: args });
}
};
// Uso del envoltorio
const safeGetUser = withErrorHandling(
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.statusText}`);
}
return response.json();
},
(error, context) => {
console.error(`Error fetching user: ${error.message}`, context);
return { error: true, message: 'Unable to retrieve user information' };
}
);
// Uso simplificado
const user = await safeGetUser(123);
if (user.error) {
// Manejar el caso de error
}
Este patrón separa la lógica de negocio del manejo de errores, mejorando la legibilidad y mantenibilidad.
Fail-fast vs. Fail-safe
Elegir entre enfoques fail-fast (fallar rápido) y fail-safe (fallar de forma segura) depende del contexto:
// Enfoque fail-fast: detiene la ejecución ante el primer error
function validateUserDataStrict(userData) {
if (!userData.name) throw new ValidationError('Name is required');
if (!userData.email) throw new ValidationError('Email is required');
if (!isValidEmail(userData.email)) throw new ValidationError('Email is invalid');
return true; // Todos los datos son válidos
}
// Enfoque fail-safe: recopila todos los errores antes de fallar
function validateUserDataCollective(userData) {
const error = new ValidationError('Validation failed');
if (!userData.name) error.addValidationError('name', 'Name is required');
if (!userData.email) error.addValidationError('email', 'Email is required');
else if (!isValidEmail(userData.email)) error.addValidationError('email', 'Email is invalid');
if (error.validationErrors.length > 0) {
throw error;
}
return true; // Todos los datos son válidos
}
El enfoque fail-fast es útil durante el desarrollo para detectar problemas rápidamente, mientras que el enfoque fail-safe proporciona mejor experiencia de usuario en producción.
Estrategia de reintentos con retroceso exponencial
Para operaciones que pueden fallar temporalmente, implementar una estrategia de reintentos puede aumentar la resiliencia:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
const { retryDelay = 300, retryMultiplier = 2 } = options;
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fetch(url, options);
} catch (error) {
lastError = error;
// Verificar si el error es recuperable
if (!isRetryableError(error)) {
throw error;
}
// Calcular retraso con retroceso exponencial
const delay = retryDelay * Math.pow(retryMultiplier, attempt);
console.warn(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`);
// Esperar antes del siguiente intento
await new Promise(resolve => setTimeout(resolve, delay));
}
}
// Si llegamos aquí, todos los intentos fallaron
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
// Función auxiliar para determinar si un error permite reintento
function isRetryableError(error) {
// Errores de red o errores de servidor (5xx) suelen ser recuperables
return error.name === 'NetworkError' ||
(error.response && error.response.status >= 500);
}
Esta estrategia es especialmente útil para operaciones de red o interacciones con servicios externos que pueden experimentar fallos transitorios.
Registro y monitoreo de errores
Un sistema robusto de manejo de errores debe incluir registro detallado para facilitar la depuración y el análisis:
function logError(error, severity = 'error', metadata = {}) {
const logEntry = {
message: error.message,
name: error.name,
stack: error.stack,
severity,
timestamp: new Date().toISOString(),
...metadata
};
// Registro local para desarrollo
console[severity](JSON.stringify(logEntry, null, 2));
// En producción, enviar a un servicio de registro
if (process.env.NODE_ENV === 'production') {
sendToLoggingService(logEntry);
}
return logEntry;
}
// Ejemplo de uso con contexto adicional
try {
processPayment(order);
} catch (error) {
logError(error, 'error', {
orderId: order.id,
userId: order.userId,
amount: order.total
});
// Manejo específico según el tipo de error
if (error instanceof PaymentGatewayError) {
notifyAdminOfPaymentIssue(error, order);
}
// Informar al usuario de manera amigable
return { success: false, message: 'Unable to process payment at this time' };
}
Un buen sistema de registro proporciona visibilidad sobre los problemas en producción y facilita la identificación de patrones de error.
Degradación elegante
Diseñar sistemas que puedan degradarse elegantemente cuando ocurren errores mejora la experiencia del usuario:
async function loadDashboardData(userId) {
const results = {
user: null,
recentActivity: [],
recommendations: [],
notifications: []
};
// Cargar datos en paralelo con manejo individual de errores
await Promise.allSettled([
// Datos críticos - si fallan, la página no es útil
fetchUserProfile(userId)
.then(data => { results.user = data; })
.catch(error => {
logError(error, 'error', { component: 'userProfile', userId });
throw error; // Re-lanzar errores críticos
}),
// Datos no críticos - la página sigue siendo útil sin ellos
fetchRecentActivity(userId)
.then(data => { results.recentActivity = data; })
.catch(error => {
logError(error, 'warn', { component: 'recentActivity', userId });
// No re-lanzar, usar datos por defecto
}),
fetchRecommendations(userId)
.then(data => { results.recommendations = data; })
.catch(error => {
logError(error, 'warn', { component: 'recommendations', userId });
// No re-lanzar, usar datos por defecto
}),
fetchNotifications(userId)
.then(data => { results.notifications = data; })
.catch(error => {
logError(error, 'warn', { component: 'notifications', userId });
// No re-lanzar, usar datos por defecto
})
]);
// Verificar si los datos críticos se cargaron correctamente
if (!results.user) {
throw new Error('Failed to load critical user data');
}
return results;
}
Este enfoque permite que la aplicación siga funcionando parcialmente incluso cuando algunos componentes fallan, priorizando la disponibilidad de las funcionalidades críticas.
Diseño de estrategias robustas para el control de errores: Patrones y mejores prácticas
El manejo efectivo de errores va más allá de simplemente capturar y lanzar excepciones. Requiere un enfoque estructurado y sistemático que permita crear aplicaciones resilientes capaces de recuperarse de situaciones inesperadas. En esta sección exploraremos patrones y mejores prácticas para implementar estrategias robustas de control de errores en JavaScript.
Jerarquía personalizada de errores
Crear una jerarquía de errores personalizada permite categorizar los problemas de forma más precisa y facilita su manejo específico:
// Clase base para errores de la aplicación
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
// Captura la pila de llamadas para mejor depuración
Error.captureStackTrace(this, this.constructor);
}
}
// Errores específicos que extienden la clase base
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
this.validationErrors = [];
}
addValidationError(field, message) {
this.validationErrors.push({ field, message });
return this;
}
}
class NotFoundError extends AppError {
constructor(entity, query) {
super(`${entity} not found with ${query}`, 404);
this.entity = entity;
this.query = query;
}
}
Esta estructura permite identificar rápidamente el tipo de error y proporciona información contextual adicional para facilitar la depuración.
Patrón de manejo centralizado de errores
Implementar un manejador centralizado de errores evita la duplicación de código y garantiza un tratamiento consistente:
// Función para manejar errores de forma centralizada
function handleError(error, context = {}) {
// Enriquece el error con información contextual
const enrichedError = {
message: error.message,
name: error.name,
stack: error.stack,
timestamp: new Date().toISOString(),
context
};
// Comportamiento específico según el tipo de error
if (error instanceof ValidationError) {
console.warn('Validation failed:', enrichedError);
// Lógica para errores de validación
} else if (error instanceof NotFoundError) {
console.info('Resource not found:', enrichedError);
// Lógica para recursos no encontrados
} else {
console.error('Unexpected error:', enrichedError);
// Lógica para errores inesperados
}
// Posible integración con sistemas de monitoreo
// reportErrorToMonitoringService(enrichedError);
return enrichedError;
}
Este enfoque permite escalar el manejo de errores y facilita la integración con servicios de monitoreo externos.
Patrón Try-Catch-Finally con limpieza de recursos
Es fundamental asegurar que los recursos se liberen adecuadamente incluso cuando ocurren errores:
async function processFile(filePath) {
let fileHandle = null;
try {
// Adquirir recursos
fileHandle = await fs.promises.open(filePath, 'r');
const content = await fileHandle.readFile('utf8');
return processContent(content);
} catch (error) {
// Manejar el error específicamente
if (error.code === 'ENOENT') {
throw new NotFoundError('File', filePath);
}
throw error; // Re-lanzar otros errores
} finally {
// Garantizar la liberación de recursos
if (fileHandle) {
await fileHandle.close().catch(err => {
console.error('Error closing file:', err);
});
}
}
}
El bloque finally
garantiza que la limpieza de recursos ocurra independientemente del resultado de la operación.
Errores específicos del dominio
Diseñar errores que reflejen el dominio de la aplicación mejora la legibilidad y el mantenimiento:
class InsufficientFundsError extends AppError {
constructor(accountId, amount, balance) {
super(`Account ${accountId} has insufficient funds for transaction of ${amount}`, 400);
this.accountId = accountId;
this.amount = amount;
this.balance = balance;
this.shortfall = amount - balance;
}
}
// Uso en el contexto de negocio
function withdrawMoney(accountId, amount) {
const account = findAccount(accountId);
if (account.balance < amount) {
throw new InsufficientFundsError(accountId, amount, account.balance);
}
// Proceder con la transacción
account.balance -= amount;
return account;
}
Estos errores proporcionan información contextual valiosa que puede utilizarse para tomar decisiones de recuperación o para informar al usuario.
Patrón de envoltorio (Wrapper) para manejo de errores
Encapsular operaciones propensas a errores en funciones de orden superior simplifica el código y reduce la duplicación:
// Función envoltorio para operaciones asíncronas
const withErrorHandling = (fn, errorHandler) => async (...args) => {
try {
return await fn(...args);
} catch (error) {
return errorHandler(error, { functionName: fn.name, arguments: args });
}
};
// Uso del envoltorio
const safeGetUser = withErrorHandling(
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.statusText}`);
}
return response.json();
},
(error, context) => {
console.error(`Error fetching user: ${error.message}`, context);
return { error: true, message: 'Unable to retrieve user information' };
}
);
// Uso simplificado
const user = await safeGetUser(123);
if (user.error) {
// Manejar el caso de error
}
Este patrón separa la lógica de negocio del manejo de errores, mejorando la legibilidad y mantenibilidad.
Fail-fast vs. Fail-safe
Elegir entre enfoques fail-fast (fallar rápido) y fail-safe (fallar de forma segura) depende del contexto:
// Enfoque fail-fast: detiene la ejecución ante el primer error
function validateUserDataStrict(userData) {
if (!userData.name) throw new ValidationError('Name is required');
if (!userData.email) throw new ValidationError('Email is required');
if (!isValidEmail(userData.email)) throw new ValidationError('Email is invalid');
return true; // Todos los datos son válidos
}
// Enfoque fail-safe: recopila todos los errores antes de fallar
function validateUserDataCollective(userData) {
const error = new ValidationError('Validation failed');
if (!userData.name) error.addValidationError('name', 'Name is required');
if (!userData.email) error.addValidationError('email', 'Email is required');
else if (!isValidEmail(userData.email)) error.addValidationError('email', 'Email is invalid');
if (error.validationErrors.length > 0) {
throw error;
}
return true; // Todos los datos son válidos
}
El enfoque fail-fast es útil durante el desarrollo para detectar problemas rápidamente, mientras que el enfoque fail-safe proporciona mejor experiencia de usuario en producción.
Estrategia de reintentos con retroceso exponencial
Para operaciones que pueden fallar temporalmente, implementar una estrategia de reintentos puede aumentar la resiliencia:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
const { retryDelay = 300, retryMultiplier = 2 } = options;
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fetch(url, options);
} catch (error) {
lastError = error;
// Verificar si el error es recuperable
if (!isRetryableError(error)) {
throw error;
}
// Calcular retraso con retroceso exponencial
const delay = retryDelay * Math.pow(retryMultiplier, attempt);
console.warn(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`);
// Esperar antes del siguiente intento
await new Promise(resolve => setTimeout(resolve, delay));
}
}
// Si llegamos aquí, todos los intentos fallaron
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
// Función auxiliar para determinar si un error permite reintento
function isRetryableError(error) {
// Errores de red o errores de servidor (5xx) suelen ser recuperables
return error.name === 'NetworkError' ||
(error.response && error.response.status >= 500);
}
Esta estrategia es especialmente útil para operaciones de red o interacciones con servicios externos que pueden experimentar fallos transitorios.
Registro y monitoreo de errores
Un sistema robusto de manejo de errores debe incluir registro detallado para facilitar la depuración y el análisis:
function logError(error, severity = 'error', metadata = {}) {
const logEntry = {
message: error.message,
name: error.name,
stack: error.stack,
severity,
timestamp: new Date().toISOString(),
...metadata
};
// Registro local para desarrollo
console[severity](JSON.stringify(logEntry, null, 2));
// En producción, enviar a un servicio de registro
if (process.env.NODE_ENV === 'production') {
sendToLoggingService(logEntry);
}
return logEntry;
}
// Ejemplo de uso con contexto adicional
try {
processPayment(order);
} catch (error) {
logError(error, 'error', {
orderId: order.id,
userId: order.userId,
amount: order.total
});
// Manejo específico según el tipo de error
if (error instanceof PaymentGatewayError) {
notifyAdminOfPaymentIssue(error, order);
}
// Informar al usuario de manera amigable
return { success: false, message: 'Unable to process payment at this time' };
}
Un buen sistema de registro proporciona visibilidad sobre los problemas en producción y facilita la identificación de patrones de error.
Degradación elegante
Diseñar sistemas que puedan degradarse elegantemente cuando ocurren errores mejora la experiencia del usuario:
async function loadDashboardData(userId) {
const results = {
user: null,
recentActivity: [],
recommendations: [],
notifications: []
};
// Cargar datos en paralelo con manejo individual de errores
await Promise.allSettled([
// Datos críticos - si fallan, la página no es útil
fetchUserProfile(userId)
.then(data => { results.user = data; })
.catch(error => {
logError(error, 'error', { component: 'userProfile', userId });
throw error; // Re-lanzar errores críticos
}),
// Datos no críticos - la página sigue siendo útil sin ellos
fetchRecentActivity(userId)
.then(data => { results.recentActivity = data; })
.catch(error => {
logError(error, 'warn', { component: 'recentActivity', userId });
// No re-lanzar, usar datos por defecto
}),
fetchRecommendations(userId)
.then(data => { results.recommendations = data; })
.catch(error => {
logError(error, 'warn', { component: 'recommendations', userId });
// No re-lanzar, usar datos por defecto
}),
fetchNotifications(userId)
.then(data => { results.notifications = data; })
.catch(error => {
logError(error, 'warn', { component: 'notifications', userId });
// No re-lanzar, usar datos por defecto
})
]);
// Verificar si los datos críticos se cargaron correctamente
if (!results.user) {
throw new Error('Failed to load critical user data');
}
return results;
}
Este enfoque permite que la aplicación siga funcionando parcialmente incluso cuando algunos componentes fallan, priorizando la disponibilidad de las funcionalidades críticas.
Aprendizajes de esta lección de JavaScript
- Diseñar una jerarquía personalizada de errores en JavaScript.
- Implementar un patrón de manejo de errores centralizado.
- Integrar bloques Try-Catch-Finally para la gestión completa de recursos.
- Aplicar patrones de envoltorio para un manejo de errores optimizado.
- Diferenciar entre enfoques fail-fast y fail-safe.
- Implementar una estrategia de reintentos con retroceso exponencial.
- Establecer sistemas de registro y monitoreo de errores.
- Diseñar sistemas con degradación elegante ante fallos.
Completa este curso de JavaScript y certifícate
Únete a nuestra plataforma de cursos de programación y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.
Asistente IA
Resuelve dudas al instante
Ejercicios
Practica con proyectos reales
Certificados
Valida tus conocimientos
Más de 25.000 desarrolladores ya se han certificado con CertiDevs