Troubleshooting Tuesday: Debugging Async/Await en JavaScript - Los 5 Errores Más Comunes
Los martes nos enfocamos en resolver problemas reales. Hoy analizamos los errores más frustrantes al trabajar con async/await en JavaScript y las técnicas sistemáticas para identificarlos y resolverlos antes de que causen problemas en producción.
Problema #1: Olvidar Await - El Silent Promise Killer
Síntomas:
- Código que continúa ejecutándose sin esperar resultados
- Variables con valores
undefinedinesperadamente - Promesas no resueltas que quedan flotando
- Comportamiento inconsistente en diferentes ejecuciones
Código Problemático:
// ❌ Olvidar await causa ejecución incorrecta
async function processUserData(userId) {
const user = getUserById(userId); // Falta await!
console.log(user.name); // Error: Cannot read property 'name' of undefined
const orders = getOrdersByUser(user.id); // Falta await!
return orders.length; // Retorna Promise en lugar del número
}
// ❌ Await en función no-async
function saveData(data) {
const result = await saveToDatabase(data); // SyntaxError!
return result;
}
Debugging Sistemático:
// ✅ Siempre verificar que las funciones async tienen await
async function processUserData(userId) {
// Await explícito
const user = await getUserById(userId);
console.log(user.name); // Ahora funciona correctamente
const orders = await getOrdersByUser(user.id);
return orders.length; // Retorna el número correcto
}
// ✅ Marcar función como async si usa await
async function saveData(data) {
const result = await saveToDatabase(data);
return result;
}
Herramienta de Detección:
// ESLint rule para detectar promesas flotantes
// .eslintrc.js
module.exports = {
rules: {
'no-floating-promises': 'error',
'@typescript-eslint/no-floating-promises': 'error'
}
};
// Custom hook para debugging de promesas
function createPromiseTracker() {
const tracked = new Set();
const originalThen = Promise.prototype.then;
Promise.prototype.then = function(...args) {
tracked.add(this);
const result = originalThen.apply(this, args);
result.finally(() => {
tracked.delete(this);
});
return result;
};
// Verificar promesas pendientes
setInterval(() => {
if (tracked.size > 0) {
console.warn(`⚠️ ${tracked.size} promesas sin resolver detectadas`);
}
}, 5000);
}
// Activar en desarrollo
if (process.env.NODE_ENV === 'development') {
createPromiseTracker();
}
Problema #2: Error Handling Incompleto en Async/Await
Síntomas:
- Errores que crashean la aplicación sin ser capturados
- Try-catch que no cubre todos los casos
- Promesas rechazadas no manejadas (UnhandledPromiseRejection)
- Error stack traces confusos
Código Problemático:
// ❌ Try-catch solo en parte del código
async function fetchMultipleResources() {
try {
const user = await getUser();
const posts = await getPosts(user.id);
// Este código está fuera del try-catch efectivo
const comments = await getComments(posts[0].id); // Si falla, no se captura
return { user, posts, comments };
} catch (error) {
console.error('Error:', error);
}
// ❌ Procesamiento post-fetch sin protección
await processData(data); // Error no capturado
}
// ❌ Catch genérico sin contexto
async function saveRecord(record) {
try {
return await database.save(record);
} catch (error) {
console.log('Error al guardar'); // No muestra qué falló ni por qué
throw error; // Re-lanza sin contexto adicional
}
}
Error Handling Robusto:
// ✅ Try-catch completo con contexto
async function fetchMultipleResources(userId) {
try {
const user = await getUser(userId);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
await processData({ user, posts, comments });
return { user, posts, comments };
} catch (error) {
// Agregar contexto al error
const enhancedError = new Error(
`Failed to fetch resources for user ${userId}: ${error.message}`
);
enhancedError.originalError = error;
enhancedError.userId = userId;
enhancedError.timestamp = new Date().toISOString();
console.error('Resource fetch error:', {
message: enhancedError.message,
stack: error.stack,
userId
});
throw enhancedError;
}
}
// ✅ Error handling con tipos específicos
async function saveRecord(record) {
try {
return await database.save(record);
} catch (error) {
// Manejar diferentes tipos de errores
if (error.code === 'ECONNREFUSED') {
throw new Error('Database connection failed. Check if database is running.');
} else if (error.code === '23505') {
throw new Error(`Record with ID ${record.id} already exists.`);
} else if (error.name === 'ValidationError') {
throw new Error(`Invalid record data: ${error.message}`);
} else {
throw new Error(`Database error: ${error.message}`);
}
}
}
Error Handler Centralizado:
// Sistema de error handling estructurado
class AsyncErrorHandler {
constructor() {
this.errorListeners = [];
this.setupGlobalHandlers();
}
setupGlobalHandlers() {
// Capturar promesas rechazadas no manejadas
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', (event) => {
console.error('🚨 Unhandled Promise Rejection:', {
reason: event.reason,
promise: event.promise,
stack: event.reason?.stack
});
this.notifyListeners({
type: 'UNHANDLED_REJECTION',
error: event.reason,
timestamp: Date.now()
});
// Prevenir que el navegador maneje el error por defecto
event.preventDefault();
});
}
// Node.js
if (typeof process !== 'undefined') {
process.on('unhandledRejection', (reason, promise) => {
console.error('🚨 Unhandled Rejection at:', promise, 'reason:', reason);
this.notifyListeners({
type: 'UNHANDLED_REJECTION',
error: reason,
timestamp: Date.now()
});
});
}
}
// Wrapper para funciones async con error handling
wrap(asyncFn, context = '') {
return async (...args) => {
try {
return await asyncFn(...args);
} catch (error) {
const enhancedError = {
message: error.message,
stack: error.stack,
context,
args: JSON.stringify(args),
timestamp: new Date().toISOString()
};
console.error(`Error in ${context}:`, enhancedError);
this.notifyListeners(enhancedError);
throw error;
}
};
}
addListener(callback) {
this.errorListeners.push(callback);
}
notifyListeners(errorInfo) {
this.errorListeners.forEach(listener => {
try {
listener(errorInfo);
} catch (err) {
console.error('Error in error listener:', err);
}
});
}
}
// Uso
const errorHandler = new AsyncErrorHandler();
// Envolver funciones críticas
const safeFetchUser = errorHandler.wrap(fetchUser, 'fetchUser');
const safeSaveData = errorHandler.wrap(saveData, 'saveData');
// Monitorear errores
errorHandler.addListener((error) => {
// Enviar a servicio de logging
sendToErrorTracking(error);
});
Problema #3: Async/Await en Loops - Secuencial vs Paralelo
Síntomas:
- Operaciones que toman demasiado tiempo
- Procesamiento lento de arrays grandes
- Requests API que se ejecutan uno por uno innecesariamente
- Performance degradada sin razón aparente
Código Problemático:
// ❌ Ejecución secuencial innecesaria
async function processUsers(userIds) {
const results = [];
// Cada iteración espera a la anterior (muy lento!)
for (const id of userIds) {
const user = await fetchUser(id); // 1 segundo cada uno
const data = await processUserData(user); // 2 segundos cada uno
results.push(data);
}
return results; // 100 users = 300 segundos!
}
// ❌ forEach con async no funciona como esperado
async function updateAllUsers(users) {
users.forEach(async (user) => {
await updateUser(user); // No se esperan estos awaits!
});
console.log('Done!'); // Se ejecuta inmediatamente, no espera
}
Solución con Ejecución Paralela:
// ✅ Ejecución paralela con Promise.all
async function processUsers(userIds) {
// Todas las operaciones inician al mismo tiempo
const userPromises = userIds.map(id => fetchUser(id));
const users = await Promise.all(userPromises);
const dataPromises = users.map(user => processUserData(user));
const results = await Promise.all(dataPromises);
return results; // 100 users = ~3 segundos (el más lento)
}
// ✅ Ejecución paralela con límite de concurrencia
async function processUsersWithLimit(userIds, limit = 5) {
const results = [];
// Procesar en chunks para evitar sobrecarga
for (let i = 0; i < userIds.length; i += limit) {
const chunk = userIds.slice(i, i + limit);
const chunkResults = await Promise.all(
chunk.map(async (id) => {
const user = await fetchUser(id);
return processUserData(user);
})
);
results.push(...chunkResults);
}
return results;
}
// ✅ for...of cuando necesitas ejecución secuencial
async function updateAllUsers(users) {
for (const user of users) {
await updateUser(user); // Espera cada uno antes de continuar
}
console.log('Done!'); // Ahora sí espera a todos
}
Utility Functions para Diferentes Scenarios:
// Promise.all - Todas deben tener éxito
async function fetchAllOrFail(urls) {
try {
const responses = await Promise.all(
urls.map(url => fetch(url))
);
return responses;
} catch (error) {
console.error('Al menos una request falló:', error);
throw error;
}
}
// Promise.allSettled - Ejecutar todas, incluso si algunas fallan
async function fetchAllWithResults(urls) {
const results = await Promise.allSettled(
urls.map(url => fetch(url))
);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
console.log(`Exitosas: ${successful.length}, Fallidas: ${failed.length}`);
return { successful, failed };
}
// Promise.race - La primera en completar
async function fetchWithTimeout(url, timeoutMs = 5000) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), timeoutMs);
});
return Promise.race([
fetch(url),
timeoutPromise
]);
}
// Batch processing con control de concurrencia
class BatchProcessor {
constructor(concurrency = 3) {
this.concurrency = concurrency;
this.queue = [];
this.running = 0;
}
async process(items, handler) {
const results = [];
return new Promise((resolve, reject) => {
let index = 0;
const processNext = async () => {
if (index >= items.length && this.running === 0) {
resolve(results);
return;
}
while (this.running < this.concurrency && index < items.length) {
const currentIndex = index++;
const item = items[currentIndex];
this.running++;
handler(item)
.then(result => {
results[currentIndex] = result;
})
.catch(error => {
console.error(`Error processing item ${currentIndex}:`, error);
results[currentIndex] = { error: error.message };
})
.finally(() => {
this.running--;
processNext();
});
}
};
processNext();
});
}
}
// Uso
const processor = new BatchProcessor(5); // 5 concurrent operations
const results = await processor.process(userIds, async (id) => {
const user = await fetchUser(id);
return processUserData(user);
});
Problema #4: Race Conditions en Async Code
Síntomas:
- Resultados inconsistentes en diferentes ejecuciones
- Datos que se sobrescriben entre sí
- Estado de UI desincronizado
- Requests obsoletos que se procesan después de los nuevos
Código Problemático:
// ❌ Race condition en búsqueda
async function handleSearch(query) {
setLoading(true);
const results = await searchAPI(query);
// Si el usuario escribió otra cosa mientras esperábamos,
// estos resultados ya no son relevantes
setResults(results);
setLoading(false);
}
// ❌ Múltiples updates concurrentes
async function incrementCounter() {
const current = await getCounter();
const newValue = current + 1;
await saveCounter(newValue); // Race condition si se llama múltiples veces
}
Solución con Request Cancellation:
// ✅ Cancelar requests obsoletos
class SearchManager {
constructor() {
this.currentController = null;
this.requestId = 0;
}
async search(query) {
// Cancelar request anterior
if (this.currentController) {
this.currentController.abort();
}
// Crear nuevo controller
this.currentController = new AbortController();
const currentRequestId = ++this.requestId;
try {
setLoading(true);
const results = await fetch(`/api/search?q=${query}`, {
signal: this.currentController.signal
});
// Verificar que este sigue siendo el request más reciente
if (currentRequestId === this.requestId) {
const data = await results.json();
setResults(data);
} else {
console.log('Descartando resultados obsoletos');
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request cancelado');
} else {
console.error('Search error:', error);
}
} finally {
if (currentRequestId === this.requestId) {
setLoading(false);
}
}
}
}
const searchManager = new SearchManager();
// Uso en input handler
function handleSearchInput(event) {
searchManager.search(event.target.value);
}
Solución con Locks/Mutexes:
// ✅ Implementar mutex para operaciones críticas
class AsyncMutex {
constructor() {
this.queue = [];
this.locked = false;
}
async acquire() {
if (!this.locked) {
this.locked = true;
return () => this.release();
}
return new Promise(resolve => {
this.queue.push(() => {
resolve(() => this.release());
});
});
}
release() {
if (this.queue.length > 0) {
const next = this.queue.shift();
next();
} else {
this.locked = false;
}
}
async runExclusive(callback) {
const release = await this.acquire();
try {
return await callback();
} finally {
release();
}
}
}
// Uso para prevenir race conditions
const counterMutex = new AsyncMutex();
async function incrementCounter() {
await counterMutex.runExclusive(async () => {
const current = await getCounter();
const newValue = current + 1;
await saveCounter(newValue);
});
}
// Múltiples llamadas ahora son seguras
await Promise.all([
incrementCounter(),
incrementCounter(),
incrementCounter()
]); // Counter será 3, no undefined
Problema #5: Debugging Async Stack Traces
Síntomas:
- Stack traces que no muestran dónde se originó el error
- Dificultad para rastrear el flujo de ejecución
- Errores que parecen venir de “nowhere”
- Console.log que no muestra el orden esperado
Código Problemático:
// ❌ Stack trace perdido
async function processData() {
const data = await fetchData();
return transformData(data); // Error aquí muestra stack limitado
}
function transformData(data) {
return data.items.map(item => {
return item.value.toUpperCase(); // Error si value es null
});
}
// Cuando falla, el stack trace no muestra el contexto completo
Debugging Mejorado:
// ✅ Async stack traces con contexto
async function processDataWithContext() {
console.log('🔵 Iniciando processData');
try {
console.log(' 📥 Fetching data...');
const data = await fetchData();
console.log(' ✅ Data fetched:', { itemCount: data.items.length });
console.log(' 🔄 Transforming data...');
const result = transformData(data);
console.log(' ✅ Transform complete');
return result;
} catch (error) {
// Agregar contexto al stack trace
console.error('❌ Error en processData:', {
message: error.message,
stack: error.stack,
phase: 'processing',
timestamp: new Date().toISOString()
});
throw error;
}
}
function transformData(data) {
return data.items.map((item, index) => {
try {
return item.value.toUpperCase();
} catch (error) {
throw new Error(
`Transform failed at index ${index}: ${error.message}`
);
}
});
}
Async Debugging Tools:
// Utility para tracing de async operations
class AsyncTracer {
constructor() {
this.traces = new Map();
this.traceId = 0;
}
start(operationName) {
const id = ++this.traceId;
this.traces.set(id, {
id,
name: operationName,
startTime: Date.now(),
events: [],
status: 'running'
});
console.log(`🟢 [${id}] Started: ${operationName}`);
return {
id,
log: (message, data) => this.log(id, message, data),
error: (error) => this.error(id, error),
complete: () => this.complete(id)
};
}
log(id, message, data = {}) {
const trace = this.traces.get(id);
if (!trace) return;
const event = {
timestamp: Date.now(),
duration: Date.now() - trace.startTime,
message,
data
};
trace.events.push(event);
console.log(` 📝 [${id}] ${message}`, data);
}
error(id, error) {
const trace = this.traces.get(id);
if (!trace) return;
trace.status = 'error';
trace.error = {
message: error.message,
stack: error.stack,
timestamp: Date.now()
};
console.error(`❌ [${id}] Error:`, {
operation: trace.name,
duration: Date.now() - trace.startTime,
error: error.message,
events: trace.events
});
}
complete(id) {
const trace = this.traces.get(id);
if (!trace) return;
trace.status = 'completed';
trace.endTime = Date.now();
trace.duration = trace.endTime - trace.startTime;
console.log(`✅ [${id}] Completed: ${trace.name} (${trace.duration}ms)`);
return trace;
}
getTrace(id) {
return this.traces.get(id);
}
getAllTraces() {
return Array.from(this.traces.values());
}
}
// Uso
const tracer = new AsyncTracer();
async function complexOperation() {
const trace = tracer.start('complexOperation');
try {
trace.log('Fetching user data');
const user = await fetchUser();
trace.log('Processing user', { userId: user.id });
const processed = await processUser(user);
trace.log('Saving results');
await saveResults(processed);
trace.complete();
return processed;
} catch (error) {
trace.error(error);
throw error;
}
}
Performance Tracking:
// Decorator para medir performance de funciones async
function measurePerformance(target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args) {
const start = performance.now();
const label = `${target.constructor.name}.${propertyKey}`;
console.time(label);
try {
const result = await originalMethod.apply(this, args);
const duration = performance.now() - start;
console.timeEnd(label);
console.log(`⏱️ ${label} completed in ${duration.toFixed(2)}ms`);
return result;
} catch (error) {
const duration = performance.now() - start;
console.timeEnd(label);
console.error(`❌ ${label} failed after ${duration.toFixed(2)}ms:`, error);
throw error;
}
};
return descriptor;
}
// Uso con classes
class DataService {
@measurePerformance
async fetchData() {
// Implementation
}
@measurePerformance
async processData(data) {
// Implementation
}
}
Herramientas de Debugging Async
Chrome DevTools Async Stack Traces:
// Habilitar en DevTools: Settings → Experiments → Enable async stack traces
// Usar debugger statements estratégicamente
async function debugAsyncFlow() {
debugger; // Pausa aquí
const data = await fetchData();
debugger; // Pausa después del fetch
const processed = await processData(data);
debugger; // Pausa después del procesamiento
return processed;
}
Node.js Async Hooks:
// Para aplicaciones Node.js
const async_hooks = require('async_hooks');
const asyncOperations = new Map();
const hook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId) {
asyncOperations.set(asyncId, {
type,
triggerAsyncId,
timestamp: Date.now()
});
},
destroy(asyncId) {
const operation = asyncOperations.get(asyncId);
if (operation) {
const duration = Date.now() - operation.timestamp;
if (duration > 1000) {
console.warn(`⚠️ Long-running async operation: ${operation.type} (${duration}ms)`);
}
}
asyncOperations.delete(asyncId);
}
});
// Activar en desarrollo
if (process.env.NODE_ENV === 'development') {
hook.enable();
}
Checklist de Debugging Async/Await
Verificación de Código:
- Todas las funciones async tienen
asynckeyword - Todas las promesas tienen
await(o se manejan con.then()) - Try-catch cubre todo el código async relevante
- Los loops con async están optimizados (paralelo vs secuencial)
- No hay race conditions en operaciones concurrentes
Error Handling:
- Errores específicos con contexto descriptivo
- Handler para
unhandledRejectionimplementado - Stack traces preservan información útil
- Errores se loggean con suficiente detalle
Performance:
- Operaciones independientes se ejecutan en paralelo
- Límites de concurrencia implementados donde necesario
- Timeouts configurados para evitar hangs
- No hay awaits innecesarios en loops
Testing:
- Tests cubren casos de éxito y error
- Tests verifican comportamiento con delays
- Race conditions probadas con requests concurrentes
- Memory leaks verificados en operaciones long-running
Pro Tips para Async/Await
1. Usa ESLint con reglas específicas:
// .eslintrc.js
module.exports = {
extends: ['plugin:promise/recommended'],
rules: {
'no-async-promise-executor': 'error',
'require-atomic-updates': 'error',
'no-await-in-loop': 'warn',
'promise/catch-or-return': 'error',
'promise/no-nesting': 'warn'
}
};
2. Tipo de retorno explícito (TypeScript):
// ✅ Tipos explícitos previenen errores
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json(); // TypeScript verifica que retorne User
}
3. Utility function para retry logic:
async function retryAsync(fn, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
console.log(`Retry ${i + 1}/${retries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
}
}
}
// Uso
const data = await retryAsync(() => fetchData(), 3, 1000);
Estadísticas de Interés
Según análisis de errores en producción:
- 45% de los bugs async/await son por olvidar
await - 28% son race conditions no manejadas
- 18% son errores no capturados correctamente
- 9% son problemas de performance por ejecución secuencial innecesaria
El tiempo promedio de debugging para errores async:
- Promises no resueltas: 30-60 minutos
- Race conditions: 1-3 horas
- Stack traces complejos: 2-4 horas
Conversación Abierta
¿Cuál de estos errores con async/await les ha dado más quebraderos de cabeza?
¿Tienen alguna técnica de debugging para async code que no mencioné?
¿Cómo manejan el testing de código async en sus proyectos?
¿Qué herramientas usan para rastrear operaciones asíncronas en producción?
El código asíncrono puede ser uno de los aspectos más desafiantes de JavaScript, pero con las herramientas y técnicas correctas, el debugging se vuelve mucho más manejable. La clave está en anticipar los problemas comunes y establecer patrones robustos desde el inicio.
Compartamos experiencias y técnicas para escribir código async más confiable y debuggeable.
#TroubleshootingTuesday javascript asyncawait #Promises debugging webdev nodejs #ErrorHandling performance
