🔧 Terça de Solução de Problemas: Depuração de Async/Await em JavaScript - Os 5 Erros Mais Comuns

:wrench: Terça de Solução de Problemas: Depuração de Async/Await em JavaScript - Os 5 Erros Mais Comuns

Nos terças-feiras, nos concentramos em resolver problemas reais. Hoje, analisamos os erros mais frustrantes ao trabalhar com async/await em JavaScript e as técnicas sistemáticas para identificá-los e resolvê-los antes que causem problemas em produção.


:police_car_light: Problema #1: Esquecer o Await - O Assassino Silencioso de Promessas

:cross_mark: Sintomas:

  • Código que continua executando sem esperar resultados
  • Variáveis com valores undefined inesperadamente
  • Promessas não resolvidas que ficam pendentes
  • Comportamento inconsistente em diferentes execuções

:magnifying_glass_tilted_left: Código Problemático:

// ❌ Esquecer await causa execução incorreta
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 em vez do número
}

// ❌ Await em função não-async
function saveData(data) {
    const result = await saveToDatabase(data); // SyntaxError!
    return result;
}

:white_check_mark: Depuração Sistemática:

// ✅ Sempre verificar que as funções async têm await
async function processUserData(userId) {
    // Await explícito
    const user = await getUserById(userId);
    console.log(user.name); // Agora funciona corretamente

    const orders = await getOrdersByUser(user.id);
    return orders.length; // Retorna o número correto
}

// ✅ Marcar função como async se usa await
async function saveData(data) {
    const result = await saveToDatabase(data);
    return result;
}

:hammer_and_wrench: Ferramenta de Detecção:

// Regra ESLint para detectar promessas pendentes
// .eslintrc.js
module.exports = {
    rules: {
        'no-floating-promises': 'error',
        '@typescript-eslint/no-floating-promises': 'error'
    }
};

// Gancho personalizado para depuração de promessas
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 promessas pendentes
    setInterval(() => {
        if (tracked.size > 0) {
            console.warn(`⚠️ ${tracked.size} promessas não resolvidas detectadas`);
        }
    }, 5000);
}

// Ativar em desenvolvimento
if (process.env.NODE_ENV === 'development') {
    createPromiseTracker();
}

:high_voltage: Problema #2: Tratamento de Erros Incompleto em Async/Await

:cross_mark: Sintomas:

  • Erros que fazem a aplicação crashar sem serem capturados
  • Try-catch que não cobre todos os casos
  • Promessas rejeitadas não tratadas (UnhandledPromiseRejection)
  • Stack traces de erros confusos

:magnifying_glass_tilted_left: Código Problemático:

// ❌ Try-catch apenas em parte do código
async function fetchMultipleResources() {
    try {
        const user = await getUser();
        const posts = await getPosts(user.id);

        // Este código está fora do try-catch eficaz
        const comments = await getComments(posts[0].id); // Se falhar, não será capturado

        return { user, posts, comments };
    } catch (error) {
        console.error('Error:', error);
    }

    // ❌ Processamento pós-fetch sem proteção
    await processData(data); // Erro não capturado
}

// ❌ Catch genérico sem contexto
async function saveRecord(record) {
    try {
        return await database.save(record);
    } catch (error) {
        console.log('Error ao salvar'); // Não mostra o que falhou nem por quê
        throw error; // Re-lança sem contexto adicional
    }
}

:white_check_mark: Tratamento de Erros Robusto:

// ✅ Try-catch completo com 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) {
        // Adicionar contexto ao erro
        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;
    }
}

// ✅ Tratamento de erros com tipos específicos
async function saveRecord(record) {
    try {
        return await database.save(record);

    } catch (error) {
        // Tratar diferentes tipos de erros
        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}`);
        }
    }
}
```### 📊 Central de Tratamento de Erros:

```javascript
// Sistema de tratamento de erros estruturado
class AsyncErrorHandler {
    constructor() {
        this.errorListeners = [];
        this.setupGlobalHandlers();
    }

    setupGlobalHandlers() {
        // Capturar promessas rejeitadas não tratadas
        if (typeof window !== 'undefined') {
            window.addEventListener('unhandledrejection', (event) => {
                console.error('🚨 Rejeição de Promessa Não Tratada:', {
                    reason: event.reason,
                    promise: event.promise,
                    stack: event.reason?.stack
                });

                this.notifyListeners({
                    type: 'UNHANDLED_REJECTION',
                    error: event.reason,
                    timestamp: Date.now()
                });

                // Prevenir que o navegador trate o erro por padrão
                event.preventDefault();
            });
        }

        // Node.js
        if (typeof process !== 'undefined') {
            process.on('unhandledRejection', (reason, promise) => {
                console.error('🚨 Rejeição Não Tratada em:', promise, 'razão:', reason);
                this.notifyListeners({
                    type: 'UNHANDLED_REJECTION',
                    error: reason,
                    timestamp: Date.now()
                });
            });
        }
    }

    // Wrapper para funções async com tratamento de erros
    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(`Erro em ${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('Erro no ouvinte de erro:', err);
            }
        });
    }
}

// Uso
const errorHandler = new AsyncErrorHandler();

// Envolver funções críticas
const safeFetchUser = errorHandler.wrap(fetchUser, 'fetchUser');
const safeSaveData = errorHandler.wrap(saveData, 'saveData');

// Monitorar erros
errorHandler.addListener((error) => {
    // Enviar para serviço de registro
    sendToErrorTracking(error);
});

:repeat_button: Problema #3: Async/Await em Loops - Sequencial vs Paralelo

:cross_mark: Sintomas:

  • Operações que levam muito tempo
  • Processamento lento de grandes arrays
  • Requests de API que são executados um por um desnecessariamente
  • Performance degradada sem razão aparente

:magnifying_glass_tilted_left: Código Problemático:

// ❌ Execução sequencial desnecessária
async function processUsers(userIds) {
    const results = [];

    // Cada iteração espera a anterior (muito lento!)
    for (const id of userIds) {
        const user = await fetchUser(id); // 1 segundo cada um
        const data = await processUserData(user); // 2 segundos cada um
        results.push(data);
    }

    return results; // 100 users = 300 segundos!
}

// ❌ forEach com async não funciona como esperado
async function updateAllUsers(users) {
    users.forEach(async (user) => {
        await updateUser(user); // Não se esperam esses awaits!
    });

    console.log('Done!'); // É executado imediatamente, não espera
}

:white_check_mark: Solução com Execução Paralela:

// ✅ Execução paralela com Promise.all
async function processUsers(userIds) {
    // Todas as operações iniciam ao mesmo tempo
    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 (o mais lento)
}

// ✅ Execução paralela com limite de concorrência
async function processUsersWithLimit(userIds, limit = 5) {
    const results = [];

    // Processar em 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 quando você precisa de execução sequencial
async function updateAllUsers(users) {
    for (const user of users) {
        await updateUser(user); // Espera cada um antes de continuar
    }

    console.log('Done!'); // Agora sim espera todos
}
```### 🎯 Funções Utilitárias para Diferentes Cenários:

```javascript
// Promise.all - Todas devem ter sucesso
async function fetchAllOrFail(urls) {
    try {
        const responses = await Promise.all(
            urls.map(url => fetch(url))
        );
        return responses;
    } catch (error) {
        console.error('Pelo menos uma requisição falhou:', error);
        throw error;
    }
}

// Promise.allSettled - Executar todas, mesmo que algumas falhem
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(`Sucesso: ${successful.length}, Falhas: ${failed.length}`);

    return { successful, failed };
}

// Promise.race - A primeira a completar
async function fetchWithTimeout(url, timeoutMs = 5000) {
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Tempo limite da requisição')), timeoutMs);
    });

    return Promise.race([
        fetch(url),
        timeoutPromise
    ]);
}

// Processamento em lote com controle de concorrência
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(`Erro ao processar item ${currentIndex}:`, error);
                            results[currentIndex] = { error: error.message };
                        })
                        .finally(() => {
                            this.running--;
                            processNext();
                        });
                }
            };

            processNext();
        });
    }
}

// Uso
const processor = new BatchProcessor(5); // 5 operações concorrentes
const results = await processor.process(userIds, async (id) => {
    const user = await fetchUser(id);
    return processUserData(user);
});

:chequered_flag: Problema #4: Condições de Corrida em Código Assíncrono

:cross_mark: Sintomas:

  • Resultados inconsistentes em diferentes execuções
  • Dados que se sobrescrevem entre si
  • Estado da UI desincronizado
  • Requisições obsoletas que são processadas após as novas

:magnifying_glass_tilted_left: Código Problemático:

// ❌ Condição de corrida em busca
async function handleSearch(query) {
    setLoading(true);

    const results = await searchAPI(query);

    // Se o usuário escreveu outra coisa enquanto esperávamos,
    // esses resultados já não são relevantes
    setResults(results);
    setLoading(false);
}

// ❌ Múltiplos updates concorrentes
async function incrementCounter() {
    const current = await getCounter();
    const newValue = current + 1;
    await saveCounter(newValue); // Condição de corrida se chamada várias vezes
}

:white_check_mark: Solução com Cancelamento de Requisições:

// ✅ Cancelar requisições obsoletas
class SearchManager {
    constructor() {
        this.currentController = null;
        this.requestId = 0;
    }

    async search(query) {
        // Cancelar requisição anterior
        if (this.currentController) {
            this.currentController.abort();
        }

        // Criar novo 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 se esta ainda é a requisição mais recente
            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('Requisição cancelada');
            } else {
                console.error('Erro de busca:', error);
            }
        } finally {
            if (currentRequestId === this.requestId) {
                setLoading(false);
            }
        }
    }
}

const searchManager = new SearchManager();

// Uso em manipulador de entrada
function handleSearchInput(event) {
    searchManager.search(event.target.value);
}

:locked: Solução com Locks/Mutexes:

// ✅ Implementar mutex para operações 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 condições de corrida
const counterMutex = new AsyncMutex();

async function incrementCounter() {
    await counterMutex.runExclusive(async () => {
        const current = await getCounter();
        const newValue = current + 1;
        await saveCounter(newValue);
    });
}

// Múltiplas chamadas agora são seguras
await Promise.all([
    incrementCounter(),
    incrementCounter(),
    incrementCounter()
]); // Contador será 3, não indefinido

:bug: Problema #5: Depuração de Stack Traces Assíncronos

:cross_mark: Sintomas:

  • Stack traces que não mostram onde o erro se originou
  • Dificuldade para rastrear o fluxo de execução
  • Erros que parecem vir de “nenhum lugar”
  • Console.log que não mostra a ordem esperada

:magnifying_glass_tilted_left: Código Problemático:

// ❌ Stack trace perdido
async function processData() {
    const data = await fetchData();
    return transformData(data); // Erro aqui mostra stack limitado
}

function transformData(data) {
    return data.items.map(item => {
        return item.value.toUpperCase(); // Erro se value é null
    });
}

// Quando falha, o stack trace não mostra o contexto completo
```### ✅ Debugging Melhorado:

```javascript
// ✅ Async stack traces com contexto
async function processDataWithContext() {
    console.log('🔵 Iniciando processData');

    try {
        console.log('  📥 Buscando dados...');
        const data = await fetchData();
        console.log('  ✅ Dados buscados:', { itemCount: data.items.length });

        console.log('  🔄 Transformando dados...');
        const result = transformData(data);
        console.log('  ✅ Transformação completa');

        return result;

    } catch (error) {
        // Adicionar contexto ao stack trace
        console.error('❌ Erro em 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(
                `Transformação falhou no índice ${index}: ${error.message}`
            );
        }
    });
}

:magnifying_glass_tilted_left: Async Debugging Tools:

// Utilitário para rastreamento de operações assíncronas
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}] Iniciado: ${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}] Erro:`, {
            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}] Concluído: ${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('Buscando dados do usuário');
        const user = await fetchUser();

        trace.log('Processando usuário', { userId: user.id });
        const processed = await processUser(user);

        trace.log('Salvando resultados');
        await saveResults(processed);

        trace.complete();
        return processed;

    } catch (error) {
        trace.error(error);
        throw error;
    }
}

:bar_chart: Performance Tracking:

// Decorador para medir o desempenho de funções assíncronas
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} concluído em ${duration.toFixed(2)}ms`);

            return result;

        } catch (error) {
            const duration = performance.now() - start;
            console.timeEnd(label);
            console.error(`❌ ${label} falhou após ${duration.toFixed(2)}ms:`, error);
            throw error;
        }
    };

    return descriptor;
}

// Uso com classes
class DataService {
    @measurePerformance
    async fetchData() {
        // Implementação
    }

    @measurePerformance
    async processData(data) {
        // Implementação
    }
}

:hammer_and_wrench: Ferramentas de Debugging Assíncrono

Chrome DevTools Async Stack Traces:

// Habilitar no DevTools: Settings → Experiments → Enable async stack traces

// Usar statements de depuração estrategicamente
async function debugAsyncFlow() {
    debugger; // Pausa aqui

    const data = await fetchData();
    debugger; // Pausa após a busca

    const processed = await processData(data);
    debugger; // Pausa após o processamento

    return processed;
}

Node.js Async Hooks:

// Para aplicações 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(`⚠️ Operação assíncrona de longa duração: ${operation.type} (${duration}ms)`);
            }
        }
        asyncOperations.delete(asyncId);
    }
});

// Ativar em desenvolvimento
if (process.env.NODE_ENV === 'development') {
    hook.enable();
}

:clipboard: Checklist de Debugging Async/Await

:white_check_mark: Verificação de Código:

  • Todas as funções assíncronas têm a palavra-chave async
  • Todas as promessas têm await (ou são tratadas com .then())
  • Try-catch cobre todo o código assíncrono relevante
  • Os loops com assíncronos estão otimizados (paralelo vs sequencial)
  • Não há condições de corrida em operações concorrentes

:white_check_mark: Tratamento de Erros:

  • Erros específicos com contexto descritivo
  • Handler para unhandledRejection implementado
  • Stack traces preservam informações úteis
  • Erros são registrados com detalhes suficientes

:white_check_mark: Desempenho:

  • Operações independentes são executadas em paralelo
  • Limites de concorrência implementados onde necessário
  • Timeouts configurados para evitar travamentos
  • Não há awaits desnecessários em loops

:white_check_mark: Testes:

  • Testes cobrem casos de sucesso e erro
  • Testes verificam comportamento com atrasos
  • Condições de corrida testadas com solicitações concorrentes
  • Vazamentos de memória verificados em operações de longa duração

:light_bulb: Pro Tips para Async/Await

1. Use ESLint com regras 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 previnem erros
async function fetchUser(id: string): Promise<User> {
    co```javascript
const response = await fetch(`/api/users/${id}`);
    return response.json(); // TypeScript verifica que retorne User
}

3. Função utilitária para lógica de retry:

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: Estatísticas de Interesse

De acordo com a análise de erros em produção:

  • 45% dos bugs async/await são por esquecer await
  • 28% são race conditions não gerenciadas
  • 18% são erros não capturados corretamente
  • 9% são problemas de performance por execução sequencial desnecessária

O tempo médio de debug para erros async:

  • Promises não resolvidas: 30-60 minutos
  • Race conditions: 1-3 horas
  • Stack traces complexos: 2-4 horas

:speech_balloon: Conversa Aberta

Qual desses erros com async/await tem dado mais dor de cabeça para vocês?

Têm alguma técnica de debug para código async que não mencionei?

Como vocês lidam com o teste de código async em seus projetos?

Quais ferramentas usam para rastrear operações assíncronas em produção?

O código assíncrono pode ser um dos aspectos mais desafiadores do JavaScript, mas com as ferramentas e técnicas corretas, o debug se torna muito mais manejável. A chave está em antecipar os problemas comuns e estabelecer padrões robustos desde o início.

Compartilhem experiências e técnicas para escrever código async mais confiável e debuggável.


#TroubleshootingTuesday javascript asyncawait #Promises debugging webdev nodejs #ErrorHandling performance