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.
Problema #1: Esquecer o Await - O Assassino Silencioso de Promessas
Sintomas:
- Código que continua executando sem esperar resultados
- Variáveis com valores
undefinedinesperadamente - Promessas não resolvidas que ficam pendentes
- Comportamento inconsistente em diferentes execuções
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;
}
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;
}
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();
}
Problema #2: Tratamento de Erros Incompleto em Async/Await
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
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
}
}
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);
});
Problema #3: Async/Await em Loops - Sequencial vs Paralelo
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
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
}
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);
});
Problema #4: Condições de Corrida em Código Assíncrono
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
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
}
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);
}
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
Problema #5: Depuração de Stack Traces Assíncronos
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
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}`
);
}
});
}
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;
}
}
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
}
}
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();
}
Checklist de Debugging Async/Await
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
Tratamento de Erros:
- Erros específicos com contexto descritivo
- Handler para
unhandledRejectionimplementado - Stack traces preservam informações úteis
- Erros são registrados com detalhes suficientes
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
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
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);
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
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
