Promises vs Async/Await: evitando o inferno de callbacks moderno
1. O Inferno de Callbacks: Origem do Problema
1.1. O padrão clássico de callbacks em JavaScript e Node.js
Antes das Promises e do async/await, o JavaScript assíncrono era dominado por callbacks — funções passadas como argumento para serem executadas após a conclusão de uma operação. Esse padrão era onipresente no Node.js, especialmente em operações de I/O, leitura de arquivos e requisições HTTP.
// Exemplo clássico de callback em Node.js
const fs = require('fs');
fs.readFile('arquivo.txt', 'utf8', (erro, dados) => {
if (erro) {
console.error('Erro ao ler arquivo:', erro);
return;
}
console.log('Conteúdo:', dados);
});
1.2. Aninhamento excessivo e perda de legibilidade (callback hell)
O problema se agravava quando múltiplas operações assíncronas dependiam umas das outras, criando estruturas profundamente aninhadas conhecidas como "callback hell" ou "pirâmide da desgraça".
// Callback hell: múltiplas operações aninhadas
fs.readFile('usuario.json', 'utf8', (erro, usuario) => {
if (erro) {
console.error('Erro:', erro);
return;
}
const usuarioId = JSON.parse(usuario).id;
db.buscarPedidos(usuarioId, (erro, pedidos) => {
if (erro) {
console.error('Erro ao buscar pedidos:', erro);
return;
}
api.enviarEmail(pedidos[0].email, (erro, resultado) => {
if (erro) {
console.error('Erro ao enviar email:', erro);
return;
}
console.log('Email enviado:', resultado);
});
});
});
1.3. Dificuldades no tratamento de erros e fluxos condicionais
Cada callback exigia verificação manual de erro, levando a código repetitivo e propenso a falhas. Fluxos condicionais tornavam-se quase impossíveis de gerenciar de forma limpa.
2. Promises: A Primeira Onda de Organização
2.1. Conceitos fundamentais: estados (pending, fulfilled, rejected) e encadeamento
Promises introduziram um objeto que representa a eventual conclusão (ou falha) de uma operação assíncrona, com três estados possíveis: pending (pendente), fulfilled (resolvida) e rejected (rejeitada).
// Criando uma Promise simples
const promessa = new Promise((resolve, reject) => {
setTimeout(() => {
const sucesso = true;
if (sucesso) {
resolve('Operação concluída com sucesso!');
} else {
reject('Algo deu errado.');
}
}, 1000);
});
2.2. Métodos .then(), .catch() e .finally() na prática
O encadeamento de Promises permitiu escrever código assíncrono de forma mais linear:
// Encadeamento de Promises
buscarUsuario(1)
.then(usuario => buscarPedidos(usuario.id))
.then(pedidos => enviarEmail(pedidos[0].email))
.then(resultado => console.log('Email enviado:', resultado))
.catch(erro => console.error('Erro:', erro))
.finally(() => console.log('Operação finalizada'));
2.3. Limitações ainda presentes: verbosidade e encadeamento profundo
Apesar da melhora, Promises ainda podiam gerar encadeamentos longos e difíceis de ler, especialmente com múltiplos níveis de dependência e tratamento de erros específicos.
3. Async/Await: Sintaxe Síncrona para Código Assíncrono
3.1. A palavra-chave async e o retorno implícito de Promises
A declaração async transforma qualquer função em uma função que retorna uma Promise, permitindo o uso de await internamente.
// Função async que retorna uma Promise implicitamente
async function obterDados() {
return 'Dados processados';
}
obterDados().then(console.log); // 'Dados processados'
3.2. await: pausando a execução sem bloquear a thread
await pausa a execução da função async até que a Promise seja resolvida, sem bloquear a thread principal:
async function processarUsuario(id) {
const usuario = await buscarUsuario(id);
const pedidos = await buscarPedidos(usuario.id);
const email = await enviarEmail(pedidos[0].email);
return email;
}
3.3. Comparação visual: mesmo fluxo com Promises vs. async/await
// Com Promises
function obterDadosUsuario(id) {
return buscarUsuario(id)
.then(usuario => buscarPerfil(usuario.perfilId))
.then(perfil => formatarResposta(usuario, perfil));
}
// Com async/await
async function obterDadosUsuario(id) {
const usuario = await buscarUsuario(id);
const perfil = await buscarPerfil(usuario.perfilId);
return formatarResposta(usuario, perfil);
}
4. Tratamento de Erros em Cada Abordagem
4.1. Erros em Promises: catch único vs. múltiplos catch encadeados
// Promise com catch único
buscarDados()
.then(processar)
.catch(erro => console.error('Erro geral:', erro));
// Promise com catches específicos
buscarDados()
.then(dados => processar(dados))
.catch(erro => {
if (erro instanceof NetworkError) {
return tratarErroRede(erro);
}
throw erro;
})
.catch(erro => console.error('Erro não tratado:', erro));
4.2. Erros em async/await: try/catch tradicional e sua clareza
async function processarDados() {
try {
const dados = await buscarDados();
const resultado = await processar(dados);
return resultado;
} catch (erro) {
if (erro instanceof NetworkError) {
return tratarErroRede(erro);
}
console.error('Erro não tratado:', erro);
throw erro;
}
}
4.3. Erros não capturados e rejeições silenciosas: como evitar
Sempre use catch em Promises ou try/catch em async/await. Para rejeições não capturadas, utilize process.on('unhandledRejection') em Node.js.
5. Execução Paralela e Sequencial
5.1. Promises com Promise.all() e Promise.race() para concorrência
// Execução paralela com Promise.all
Promise.all([
buscarUsuario(1),
buscarUsuario(2),
buscarUsuario(3)
]).then(([usuario1, usuario2, usuario3]) => {
console.log('Todos os usuários:', usuario1, usuario2, usuario3);
});
// Promise.race: a primeira Promise resolvida vence
Promise.race([
fazerRequisicao('servidor1.com'),
fazerRequisicao('servidor2.com')
]).then(resposta => console.log('Resposta mais rápida:', resposta));
5.2. Async/await com Promise.all(): o melhor dos dois mundos
async function buscarTodosUsuarios(ids) {
const promessas = ids.map(id => buscarUsuario(id));
const usuarios = await Promise.all(promessas);
return usuarios;
}
5.3. Loops assíncronos: for...of com await vs. Promise.all() em arrays
// Sequencial (lento)
async function processarSequencial(itens) {
for (const item of itens) {
await processarItem(item);
}
}
// Paralelo (rápido)
async function processarParalelo(itens) {
await Promise.all(itens.map(item => processarItem(item)));
}
6. Casos de Uso Avançados e Boas Práticas
6.1. Timeouts e cancelamento de Promises (AbortController)
async function buscarComTimeout(url, tempoLimite = 5000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), tempoLimite);
try {
const resposta = await fetch(url, { signal: controller.signal });
return await resposta.json();
} finally {
clearTimeout(timeout);
}
}
6.2. Transformação de APIs baseadas em callbacks em Promises (promisify)
const { promisify } = require('util');
const fs = require('fs');
const readFileAsync = promisify(fs.readFile);
async function lerArquivo(caminho) {
const conteudo = await readFileAsync(caminho, 'utf8');
return conteudo;
}
6.3. Quando evitar async/await: alta concorrência e performance crítica
Em cenários de alta concorrência com milhares de operações simultâneas, Promises puras podem oferecer melhor performance devido ao menor overhead de contexto.
7. Inferno Moderno: Armadilhas Comuns com Async/Await
7.1. await sequencial desnecessário (serialização acidental)
// Ruim: serialização desnecessária
async function processarRuim(itens) {
const resultados = [];
for (const item of itens) {
const resultado = await processarItem(item); // Aguarda cada um
resultados.push(resultado);
}
return resultados;
}
// Bom: paralelismo real
async function processarBom(itens) {
const promessas = itens.map(item => processarItem(item));
return await Promise.all(promessas);
}
7.2. Promessas esquecidas sem await (fire-and-forget problemático)
// Perigoso: Promise não aguardada
async function salvarDados(dados) {
salvarNoBanco(dados); // Esqueceu o await! Erro pode passar despercebido
return 'Dados salvos';
}
// Correto
async function salvarDados(dados) {
await salvarNoBanco(dados);
return 'Dados salvos';
}
7.3. Misturar estilos no mesmo projeto: inconsistência e bugs sutis
Escolha um padrão e mantenha consistência. Misturar Promises com async/await em um mesmo fluxo pode levar a bugs difíceis de rastrear.
8. Conclusão: Escolhendo a Ferramenta Certa
8.1. Resumo das vantagens e desvantagens de cada abordagem
Promises:
- Vantagens: Excelente para paralelismo (Promise.all, Promise.race), menor overhead em alta concorrência
- Desvantagens: Verbosidade, encadeamento pode ficar confuso
Async/Await:
- Vantagens: Legibilidade superior, tratamento de erros natural com try/catch, fluxo linear
- Desvantagens: Pode induzir serialização acidental, overhead em loops grandes
8.2. Guia prático: quando usar Promises puras vs. async/await
- Use async/await para fluxos sequenciais, tratamento de erros complexo e quando a legibilidade é prioridade
- Use Promises puras para paralelismo intenso, operações de alta concorrência e quando você precisa de
Promise.allSettledouPromise.any - Combine ambos quando necessário: async/await para o fluxo principal e
Promise.all()para paralelismo
8.3. Tendências futuras: top-level await e novas APIs assíncronas
O top-level await (disponível em módulos ES) permite usar await fora de funções async, simplificando ainda mais o código. Novas APIs como Promise.withResolvers() e Promise.try() continuam evoluindo o ecossistema assíncrono.
// Top-level await em módulos ES
const dados = await fetch('https://api.exemplo.com/dados');
console.log(dados);
A escolha entre Promises e async/await não é binária — ambas são ferramentas poderosas no arsenal do desenvolvedor JavaScript moderno. O segredo está em entender quando cada uma brilha e aplicá-las com sabedoria para evitar o inferno de callbacks moderno.
Referências
-
MDN Web Docs: Using Promises — Guia completo sobre Promises em JavaScript, com exemplos práticos e explicações detalhadas dos métodos
.then(),.catch()e.finally(). -
MDN Web Docs: async function — Documentação oficial sobre funções async e o operador await, incluindo comportamento e casos de uso.
-
Node.js Documentation: Promises in Node.js — Guia oficial do Node.js sobre Promises, com exemplos de promisify e tratamento de erros em operações assíncronas.
-
JavaScript.info: Async/Await — Tutorial abrangente sobre async/await, com exemplos comparativos entre Promises e async/await, incluindo tratamento de erros e execução paralela.
-
Google Web Dev: Promises vs Async/Await — Artigo técnico do Google sobre as diferenças entre Promises e async/await, com foco em performance e boas práticas para desenvolvimento web.