50% OFF Plus
--:--:--
¡Obtener!

Excepciones

Avanzado
JavaScript
JavaScript
Actualizado: 14/05/2025

¡Desbloquea el curso de JavaScript completo!

IA
Ejercicios
Certificado
Entrar

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 Plus

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.

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.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

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

  1. Diseñar una jerarquía personalizada de errores en JavaScript.
  2. Implementar un patrón de manejo de errores centralizado.
  3. Integrar bloques Try-Catch-Finally para la gestión completa de recursos.
  4. Aplicar patrones de envoltorio para un manejo de errores optimizado.
  5. Diferenciar entre enfoques fail-fast y fail-safe.
  6. Implementar una estrategia de reintentos con retroceso exponencial.
  7. Establecer sistemas de registro y monitoreo de errores.
  8. 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

⭐⭐⭐⭐⭐
4.9/5 valoración