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