TechLead
Intermedio
20 min
Lección 7 de 10
API

Manejo de Errores de API

Maneja errores de API de forma elegante con códigos de error apropiados, reintentos y estrategias de respaldo

Por qué Importa el Manejo de Errores

Las APIs pueden fallar por muchas razones: problemas de red, errores del servidor, límites de tasa, datos inválidos o problemas de autenticación. Un manejo de errores adecuado asegura que tu aplicación se mantenga estable, proporcione retroalimentación útil a los usuarios y pueda recuperarse elegantemente de las fallas.

Una estrategia de manejo de errores bien diseñada mejora la experiencia del usuario y facilita mucho la depuración.

Tipos de Errores de API

❌ Errores de Red

  • • Sin conexión a internet
  • • Resolución DNS fallida
  • • Tiempo de espera agotado
  • • Problemas CORS

⚠️ Errores del Cliente (4xx)

  • • 400 - Datos de solicitud incorrectos
  • • 401 - No autenticado
  • • 403 - No autorizado
  • • 404 - Recurso no encontrado
  • • 429 - Límite de tasa excedido

🔥 Errores del Servidor (5xx)

  • • 500 - Error interno del servidor
  • • 502 - Puerta de enlace incorrecta
  • • 503 - Servicio no disponible
  • • 504 - Tiempo de espera de puerta de enlace agotado

📦 Errores de Datos

  • • Respuesta JSON inválida
  • • Formato de datos inesperado
  • • Campos requeridos faltantes
  • • Desajustes de tipo

Patrón Básico de Manejo de Errores

// Manejo integral de errores de fetch
async function fetchWithErrorHandling(url, options = {}) {
  try {
    const response = await fetch(url, options);

    // La solicitud de red tuvo éxito, pero necesitamos verificar el estado HTTP
    if (!response.ok) {
      // Intentar analizar detalles del error de la respuesta
      let errorData;
      try {
        errorData = await response.json();
      } catch {
        errorData = { message: response.statusText };
      }

      // Crear error detallado
      const error = new Error(errorData.message || 'Solicitud fallida');
      error.status = response.status;
      error.statusText = response.statusText;
      error.data = errorData;
      throw error;
    }

    // Analizar y devolver respuesta exitosa
    const contentType = response.headers.get('content-type');
    if (contentType?.includes('application/json')) {
      return await response.json();
    }
    return await response.text();

  } catch (error) {
    // Manejar diferentes tipos de errores
    if (error.name === 'TypeError') {
      // Error de red (sin internet, CORS, etc.)
      throw new NetworkError('No se puede conectar al servidor');
    }
    
    if (error.name === 'AbortError') {
      throw new TimeoutError('La solicitud fue cancelada');
    }
    
    // Re-lanzar errores HTTP
    throw error;
  }
}

// Clases de error personalizadas
class ApiError extends Error {
  constructor(message, status, data) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
    this.data = data;
  }
}

class NetworkError extends ApiError {
  constructor(message) {
    super(message, 0);
    this.name = 'NetworkError';
  }
}

class TimeoutError extends ApiError {
  constructor(message) {
    super(message, 0);
    this.name = 'TimeoutError';
  }
}

class ValidationError extends ApiError {
  constructor(message, errors) {
    super(message, 422);
    this.name = 'ValidationError';
    this.errors = errors;
  }
}

Manejo Específico según Código de Estado

// Manejar errores basados en código de estado
async function handleApiError(error) {
  switch (error.status) {
    case 400:
      // Solicitud incorrecta - mostrar errores de validación
      showValidationErrors(error.data.errors);
      break;

    case 401:
      // No autorizado - redirigir al inicio de sesión
      clearAuthTokens();
      redirectToLogin();
      break;

    case 403:
      // Prohibido - mostrar permiso denegado
      showMessage('No tienes permiso para realizar esta acción');
      break;

    case 404:
      // No encontrado
      showMessage('El recurso solicitado no fue encontrado');
      break;

    case 409:
      // Conflicto - el recurso ya existe
      showMessage('Este elemento ya existe');
      break;

    case 422:
      // Error de validación
      showValidationErrors(error.data.errors);
      break;

    case 429:
      // Límite de tasa excedido
      const retryAfter = error.data.retryAfter || 60;
      showMessage(`Demasiadas solicitudes. Intenta de nuevo en ${retryAfter} segundos`);
      break;

    case 500:
    case 502:
    case 503:
    case 504:
      // Error del servidor - mostrar error genérico, quizás reintentar
      showMessage('Algo salió mal. Por favor intenta más tarde.');
      logErrorToService(error);
      break;

    default:
      showMessage('Ocurrió un error inesperado');
      logErrorToService(error);
  }
}

// Uso en componentes
async function fetchUserProfile() {
  try {
    const profile = await fetchWithErrorHandling('/api/profile');
    displayProfile(profile);
  } catch (error) {
    handleApiError(error);
  }
}

Lógica de Reintento con Retroceso Exponencial

// Reintentar solicitudes fallidas con retrasos crecientes
async function fetchWithRetry(url, options = {}, config = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 30000,
    retryableStatuses = [408, 429, 500, 502, 503, 504]
  } = config;

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (!response.ok) {
        const error = new Error('Solicitud fallida');
        error.status = response.status;
        error.response = response;
        throw error;
      }

      return await response.json();

    } catch (error) {
      lastError = error;

      // Verificar si el error es reintentable
      const isRetryable = 
        error.name === 'TypeError' || // Error de red
        retryableStatuses.includes(error.status);

      // No reintentar si no es reintentable o es el último intento
      if (!isRetryable || attempt === maxRetries) {
        throw error;
      }

      // Calcular retraso con retroceso exponencial + variación aleatoria
      const delay = Math.min(
        baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
        maxDelay
      );

      console.log(`Reintento ${attempt + 1}/${maxRetries} después de ${delay}ms`);
      await sleep(delay);
    }
  }

  throw lastError;
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Uso
try {
  const data = await fetchWithRetry('/api/data', {}, {
    maxRetries: 5,
    baseDelay: 500
  });
} catch (error) {
  console.error('Todos los reintentos fallaron:', error);
}

Manejo de Tiempo de Espera de Solicitud

// Agregar tiempo de espera a solicitudes fetch
async function fetchWithTimeout(url, options = {}, timeout = 10000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });

    clearTimeout(timeoutId);
    return response;

  } catch (error) {
    clearTimeout(timeoutId);

    if (error.name === 'AbortError') {
      throw new TimeoutError(`Tiempo de espera agotado después de ${timeout}ms`);
    }

    throw error;
  }
}

// Combinar tiempo de espera con reintento
async function robustFetch(url, options = {}) {
  const config = {
    timeout: 10000,
    maxRetries: 3,
    ...options
  };

  return fetchWithRetry(
    url,
    {
      ...options,
      signal: AbortSignal.timeout(config.timeout)
    },
    { maxRetries: config.maxRetries }
  );
}

// Uso
try {
  const data = await robustFetch('/api/slow-endpoint', {
    timeout: 30000,  // tiempo de espera de 30 segundos
    maxRetries: 5    // Reintentar hasta 5 veces
  });
} catch (error) {
  if (error instanceof TimeoutError) {
    showMessage('La solicitud está tardando demasiado. Por favor intenta de nuevo.');
  }
}

Estrategias de Respaldo

// Patrones de respaldo para aplicaciones resilientes

// 1. Respaldo de caché - usar datos en caché cuando la API falla
async function fetchWithCacheFallback(key, fetchFn) {
  try {
    const data = await fetchFn();
    
    // Almacenar en caché la respuesta exitosa
    localStorage.setItem(key, JSON.stringify({
      data,
      timestamp: Date.now()
    }));
    
    return data;
    
  } catch (error) {
    console.warn('API falló, intentando caché:', error);
    
    // Intentar usar datos en caché
    const cached = localStorage.getItem(key);
    if (cached) {
      const { data, timestamp } = JSON.parse(cached);
      console.log(`Usando datos en caché desde ${new Date(timestamp)}`);
      return data;
    }
    
    throw error;
  }
}

// 2. Respaldo de valor predeterminado
async function fetchWithDefault(fetchFn, defaultValue) {
  try {
    return await fetchFn();
  } catch (error) {
    console.warn('Usando valor predeterminado debido a error:', error);
    return defaultValue;
  }
}

// 3. Endpoint de API de respaldo
async function fetchWithFallbackEndpoint(primaryUrl, fallbackUrl, options) {
  try {
    return await fetchWithErrorHandling(primaryUrl, options);
  } catch (error) {
    console.warn('API principal falló, intentando respaldo:', error);
    return await fetchWithErrorHandling(fallbackUrl, options);
  }
}

// 4. Degradación elegante
async function loadDashboard() {
  const [userData, postsData, statsData] = await Promise.allSettled([
    fetchWithDefault(() => api.get('/user'), null),
    fetchWithDefault(() => api.get('/posts'), []),
    fetchWithDefault(() => api.get('/stats'), { views: 0, likes: 0 })
  ]);

  return {
    user: userData.value,
    posts: postsData.value,
    stats: statsData.value
  };
}

// Uso
const dashboard = await loadDashboard();
// Incluso si algunas APIs fallan, el panel se carga con valores predeterminados

Mensajes de Error Amigables para el Usuario

// Mapear errores técnicos a mensajes amigables para el usuario
const errorMessages = {
  network: {
    title: 'Problema de Conexión',
    message: 'Por favor verifica tu conexión a internet e intenta de nuevo.',
    action: 'Reintentar'
  },
  timeout: {
    title: 'Tiempo de Espera Agotado',
    message: 'El servidor está tardando demasiado en responder. Por favor intenta de nuevo.',
    action: 'Reintentar'
  },
  unauthorized: {
    title: 'Sesión Expirada',
    message: 'Por favor inicia sesión de nuevo para continuar.',
    action: 'Iniciar Sesión'
  },
  forbidden: {
    title: 'Acceso Denegado',
    message: 'No tienes permiso para acceder a este recurso.',
    action: 'Volver'
  },
  notFound: {
    title: 'No Encontrado',
    message: 'El elemento solicitado no pudo ser encontrado.',
    action: 'Ir al Inicio'
  },
  validation: {
    title: 'Datos Inválidos',
    message: 'Por favor verifica tu entrada e intenta de nuevo.',
    action: 'Corregir Errores'
  },
  rateLimit: {
    title: 'Demasiadas Solicitudes',
    message: 'Por favor espera un momento antes de intentar de nuevo.',
    action: 'Esperar'
  },
  server: {
    title: 'Error del Servidor',
    message: 'Algo salió mal de nuestro lado. Por favor intenta más tarde.',
    action: 'Reintentar'
  },
  unknown: {
    title: 'Error',
    message: 'Ocurrió un error inesperado.',
    action: 'Reintentar'
  }
};

function getErrorDisplay(error) {
  if (error instanceof NetworkError) {
    return errorMessages.network;
  }
  
  if (error instanceof TimeoutError) {
    return errorMessages.timeout;
  }

  switch (error.status) {
    case 401: return errorMessages.unauthorized;
    case 403: return errorMessages.forbidden;
    case 404: return errorMessages.notFound;
    case 422: return errorMessages.validation;
    case 429: return errorMessages.rateLimit;
    case 500:
    case 502:
    case 503:
    case 504: return errorMessages.server;
    default: return errorMessages.unknown;
  }
}

// Componente de visualización de errores (ejemplo de React)
function ErrorDisplay({ error, onRetry, onDismiss }) {
  const { title, message, action } = getErrorDisplay(error);
  
  return (
    

{title}

{message}

{error.status === 422 && error.errors && (
    {error.errors.map(e =>
  • {e.message}
  • )}
)}
); }

Manejador Global de Errores

// Manejo centralizado de errores para toda la aplicación
class ErrorHandler {
  constructor() {
    this.errorListeners = [];
    this.errorLog = [];
  }

  // Registrar oyente de errores
  onError(callback) {
    this.errorListeners.push(callback);
    return () => {
      this.errorListeners = this.errorListeners.filter(cb => cb !== callback);
    };
  }

  // Manejar un error
  handle(error, context = {}) {
    // Registrar error
    const errorEntry = {
      error,
      context,
      timestamp: new Date().toISOString(),
      url: window.location.href
    };
    this.errorLog.push(errorEntry);

    // Notificar oyentes
    this.errorListeners.forEach(listener => {
      try {
        listener(error, context);
      } catch (e) {
        console.error('Error en oyente de errores:', e);
      }
    });

    // Enviar al servicio de seguimiento de errores
    this.reportToService(errorEntry);

    // Devolver error amigable para el usuario
    return getErrorDisplay(error);
  }

  async reportToService(errorEntry) {
    try {
      await fetch('/api/errors', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(errorEntry)
      });
    } catch {
      // Fallar silenciosamente - no causar más errores
    }
  }
}

// Instancia global
const errorHandler = new ErrorHandler();

// Configurar oyente global
errorHandler.onError((error, context) => {
  // Mostrar notificación toast
  showToast(getErrorDisplay(error).message, 'error');
});

// Uso en llamadas API
async function fetchData() {
  try {
    return await api.get('/data');
  } catch (error) {
    errorHandler.handle(error, { action: 'fetchData' });
    throw error;
  }
}

💡 Mejores Prácticas de Manejo de Errores

  • Nunca fallar silenciosamente - Siempre informa a los usuarios sobre errores
  • Usar tipos de error específicos - Distinguir errores de red, autenticación, validación
  • Implementar reintentos cuidadosamente - Solo para operaciones idempotentes
  • Mostrar mensajes accionables - Indica a los usuarios qué pueden hacer
  • Registrar errores para depuración - Incluir contexto y trazas de pila
  • Proporcionar respaldos - Caché, valores predeterminados o degradación elegante