Troubleshooting Tuesday: Debugging Async/Await - Los 5 Errores Más Comunes
Los martes nos enfocamos en resolver problemas reales. Hoy analizamos los errores más frecuentes con async/await en JavaScript y cómo solucionarlos efectivamente.
Error #1: Promise Hell (El Nuevo Callback Hell)
Problema:
async function fetchUserData() {
try {
const user = await fetch('/api/user');
const userData = await user.json();
if (userData.hasProfile) {
const profile = await fetch(`/api/profile/${userData.id}`);
const profileData = await profile.json();
if (profileData.hasPreferences) {
const prefs = await fetch(`/api/preferences/${userData.id}`);
const prefsData = await prefs.json();
return { userData, profileData, prefsData };
}
}
} catch (error) {
console.log("Error en algún lugar...");
}
}
Síntomas:
- Código difícil de leer y mantener
- Error handling poco específico
- Requests secuenciales innecesarios
Solución:
async function fetchUserData() {
try {
// Paso 1: Obtener usuario
const userResponse = await fetch('/api/user');
if (!userResponse.ok) {
throw new Error(`Error fetching user: ${userResponse.status}`);
}
const userData = await userResponse.json();
// Paso 2: Preparar requests paralelos
const requests = [Promise.resolve(userData)];
if (userData.hasProfile) {
requests.push(fetchProfile(userData.id));
}
if (userData.hasProfile) { // Solo si tiene profile
requests.push(fetchPreferences(userData.id));
}
// Paso 3: Ejecutar en paralelo
const [user, profile, preferences] = await Promise.all(requests);
return { user, profile, preferences };
} catch (error) {
console.error('Error específico en fetchUserData:', error.message);
throw new Error(`Failed to fetch user data: ${error.message}`);
}
}
async function fetchProfile(userId) {
const response = await fetch(`/api/profile/${userId}`);
if (!response.ok) throw new Error(`Profile fetch failed: ${response.status}`);
return response.json();
}
async function fetchPreferences(userId) {
const response = await fetch(`/api/preferences/${userId}`);
if (!response.ok) throw new Error(`Preferences fetch failed: ${response.status}`);
return response.json();
}
Error #2: No Manejar Timeouts
Problema:
// API lenta = aplicación colgada
const data = await fetch('/api/slow-endpoint');
Solución con AbortController:
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
// Timeout automático
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
}
}
// Uso
try {
const data = await fetchWithTimeout('/api/slow-endpoint', 3000);
} catch (error) {
console.error('API call failed:', error.message);
}
Error #3: Loops Async Incorrectos
Problema - forEach con async:
// ¡No funciona como esperas!
const users = ['1', '2', '3'];
users.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user); // Se ejecutan todas al mismo tiempo
});
Soluciones según el caso:
Secuencial (uno después del otro):
const users = [];
for (const id of ['1', '2', '3']) {
const user = await fetchUser(id);
users.push(user);
console.log(`Procesado usuario ${id}`);
}
// O con reduce para más control
const users = await ['1', '2', '3'].reduce(async (prevPromise, id) => {
const acc = await prevPromise;
const user = await fetchUser(id);
acc.push(user);
return acc;
}, Promise.resolve([]));
Paralelo (todos al mismo tiempo):
const users = await Promise.all(
['1', '2', '3'].map(id => fetchUser(id))
);
// Con manejo de errores individual
const users = await Promise.allSettled(
['1', '2', '3'].map(async (id) => {
try {
return await fetchUser(id);
} catch (error) {
console.error(`Error fetching user ${id}:`, error);
return null;
}
})
);
const successfulUsers = users
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
Error #4: No Propagar Errores Correctamente
Problema:
async function saveUser(userData) {
try {
await api.saveUser(userData);
console.log("Usuario guardado"); // ¿Pero se guardó realmente?
} catch (error) {
console.log("Error guardando usuario");
// ¡Error se pierde aquí!
}
}
// En el componente
await saveUser(data); // No sabes si funcionó
Solución:
async function saveUser(userData) {
try {
const result = await api.saveUser(userData);
console.log("✅ Usuario guardado exitosamente:", result.id);
return { success: true, data: result };
} catch (error) {
console.error("❌ Error guardando usuario:", error.message);
// Re-throw con contexto adicional
throw new Error(`Failed to save user: ${error.message}`);
}
}
// En el componente
try {
const result = await saveUser(data);
showSuccessMessage("Usuario guardado correctamente");
} catch (error) {
showErrorMessage(error.message);
// Opcional: reportar error a servicio de logging
errorLogger.report(error);
}
Error #5: Race Conditions en Estado
Problema:
let isLoading = false;
async function loadData() {
if (isLoading) return; // Race condition aquí
isLoading = true;
const data = await fetchData();
isLoading = false;
updateUI(data);
}
Solución con Request ID:
class DataLoader {
constructor() {
this.currentRequestId = 0;
this.isLoading = false;
}
async loadData() {
// Generar ID único para esta request
const requestId = ++this.currentRequestId;
if (this.isLoading) {
console.log('Request already in progress, ignoring');
return;
}
this.isLoading = true;
try {
const data = await fetchData();
// Verificar si somos la request más reciente
if (requestId === this.currentRequestId) {
updateUI(data);
} else {
console.log('Discarding stale response');
}
} finally {
this.isLoading = false;
}
}
}
const loader = new DataLoader();
// Múltiples llamadas rápidas
loader.loadData(); // Se ejecuta
loader.loadData(); // Se ignora
loader.loadData(); // Se ignora
Herramientas de Debugging Async
Wrapper para logging automático:
function asyncLogger(fn, name) {
return async function(...args) {
console.log(`🚀 Iniciando ${name}`, args);
const startTime = performance.now();
try {
const result = await fn.apply(this, args);
const duration = performance.now() - startTime;
console.log(`✅ ${name} completado en ${duration.toFixed(2)}ms`, result);
return result;
} catch (error) {
const duration = performance.now() - startTime;
console.error(`❌ ${name} falló después de ${duration.toFixed(2)}ms:`, error);
throw error;
}
};
}
// Uso
const fetchUser = asyncLogger(async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
}, 'fetchUser');
Quick Fix Checklist
Cuando debuggees código async, verifica:
Error handling específico en cada nivel
Timeouts para evitar requests colgados
Requests paralelos cuando sea posible
Race condition protection en estado compartido
Logging con contexto y timing
Response validation antes de usar datos
¿Qué error async les ha dado más dolores de cabeza? ¿Han encontrado otras soluciones creativas? Compartamos experiencias de debugging para aprender juntos.
#TroubleshootingTuesday asyncawait javascript debugging #ErrorHandling webdev