🔧 Troubleshooting Tuesday: Debugging Async/Await en JavaScript - Los 5 Errores Más Comunes

:wrench: 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.


:police_car_light: Problema #1: Olvidar Await - El Silent Promise Killer

:cross_mark: Síntomas:

  • Código que continúa ejecutándose sin esperar resultados
  • Variables con valores undefined inesperadamente
  • Promesas no resueltas que quedan flotando
  • Comportamiento inconsistente en diferentes ejecuciones

:magnifying_glass_tilted_left: 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;
}

:white_check_mark: 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;
}

:hammer_and_wrench: 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();
}

:high_voltage: Problema #2: Error Handling Incompleto en Async/Await

:cross_mark: 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

:magnifying_glass_tilted_left: 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
    }
}

:white_check_mark: 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}`);
        }
    }
}

:bar_chart: 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);
});

:repeat_button: Problema #3: Async/Await en Loops - Secuencial vs Paralelo

:cross_mark: 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

:magnifying_glass_tilted_left: 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
}

:white_check_mark: 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
}

:bullseye: 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);
});

:chequered_flag: Problema #4: Race Conditions en Async Code

:cross_mark: 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

:magnifying_glass_tilted_left: 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
}

:white_check_mark: 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);
}

:locked: 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

:bug: Problema #5: Debugging Async Stack Traces

:cross_mark: 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

:magnifying_glass_tilted_left: 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

:white_check_mark: 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}`
            );
        }
    });
}

:magnifying_glass_tilted_left: 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;
    }
}

:bar_chart: 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
    }
}

:hammer_and_wrench: 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();
}

:clipboard: Checklist de Debugging Async/Await

:white_check_mark: Verificación de Código:

  • Todas las funciones async tienen async keyword
  • 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

:white_check_mark: Error Handling:

  • Errores específicos con contexto descriptivo
  • Handler para unhandledRejection implementado
  • Stack traces preservan información útil
  • Errores se loggean con suficiente detalle

:white_check_mark: 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

:white_check_mark: 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

:light_bulb: 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);

:bar_chart: 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

:speech_balloon: 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