O Problema das Callbacks e Promises
Antes de entender async/await, você precisa conhecer o contexto. JavaScript executa código de forma assíncrona naturalmente — operações como requisições HTTP, leitura de arquivos e timers não bloqueiam a execução. Historicamente, usávamos callbacks para lidar com essas operações, mas isso levava ao famoso "callback hell": código aninhado, difícil de ler e manter. As Promises vieram como solução, permitindo uma sintaxe mais limpa com .then() e .catch(), mas ainda não era intuitivo como código síncrono.
// Callback Hell (difícil de ler)
fetchUser(userId, function(err, user) {
if (err) console.error(err);
fetchPosts(user.id, function(err, posts) {
if (err) console.error(err);
fetchComments(posts[0].id, function(err, comments) {
console.log(comments);
});
});
});
// Com Promises (melhor, mas ainda verboso)
fetchUser(userId)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log(comments))
.catch(err => console.error(err));
Entendendo Async/Await
Async/await é açúcar sintático sobre Promises que permite escrever código assíncrono com aparência síncrona. Uma função async sempre retorna uma Promise, e a palavra-chave await pausa a execução até que uma Promise seja resolvida. Isso torna o fluxo de controle muito mais legível e intuitivo para quem está acostumado com programação síncrona tradicional.
// Agora a mesma lógica é clara e linear
async function processUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
console.log(comments);
} catch (err) {
console.error(err);
}
}
// Chamar uma função async
processUserData(1);
A função fetchUser, fetchPosts e fetchComments retornam Promises. O await pausa a execução até que cada uma seja resolvida, e o resultado é atribuído diretamente à variável. Se alguma Promise for rejeitada, o bloco catch intercepta o erro. Isso é muito mais legível do que encadear múltiplos .then().
Padrões Avançados e Armadilhas Comuns
Paralelismo vs. Sequência
Um erro comum é usar await desnecessariamente em sequência quando operações podem rodar em paralelo. Se você tem duas requisições independentes, executá-las em série é ineficiente. Use Promise.all() para operações que não dependem uma da outra.
// ❌ Lento: espera 2 segundos (1s + 1s)
async function lento() {
const user = await fetchUser(1); // 1 segundo
const settings = await fetchSettings(1); // 1 segundo
return { user, settings };
}
// ✅ Rápido: espera ~1 segundo
async function rapido() {
const [user, settings] = await Promise.all([
fetchUser(1),
fetchSettings(1)
]);
return { user, settings };
}
Tratamento de Erros
O try/catch é o padrão recomendado, mas você pode também encadear .catch() em uma Promise. Para erros em múltiplas operações paralelas, Promise.all() rejeita assim que uma falha, enquanto Promise.allSettled() espera todas terminarem (sucesso ou falha).
// Promise.allSettled aguarda todas as operações
async function tratarMultiplosErros() {
const resultados = await Promise.allSettled([
fetchUser(1),
fetchUser(2),
fetchUser(999) // vai falhar
]);
resultados.forEach((resultado, index) => {
if (resultado.status === 'fulfilled') {
console.log(`User ${index}:`, resultado.value);
} else {
console.log(`User ${index} falhou:`, resultado.reason);
}
});
}
Loops Assíncrono
Quando você precisa iterar sobre um array e aguardar operações dentro do loop, cuidado: forEach() não aguarda promises. Use for...of ou .reduce() para controlar a sequência.
// ❌ Incorreto: não aguarda as promises
[1, 2, 3].forEach(async (id) => {
await fetchUser(id); // executa em paralelo, não em sequência
});
// ✅ Correto: aguarda sequencialmente
async function buscarSequencialmente() {
for (const id of [1, 2, 3]) {
const user = await fetchUser(id);
console.log(user);
}
}
// ✅ Alternativa: em paralelo com Promise.all()
async function buscarEmParalelo() {
const usuarios = await Promise.all(
[1, 2, 3].map(id => fetchUser(id))
);
console.log(usuarios);
}
Funções Auxiliares: Implementação Real
// Simular requisições HTTP (para teste)
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: `User ${id}` });
} else {
reject(new Error('ID inválido'));
}
}, 1000);
});
}
// Usar em uma aplicação real
async function main() {
try {
const user = await fetchUser(1);
console.log('Usuário encontrado:', user);
const multiplos = await Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
console.log('Múltiplos usuários:', multiplos);
} catch (err) {
console.error('Erro:', err.message);
}
}
main();
Conclusão
Async/await revoluciona a forma como lidamos com código assíncrono em JavaScript, tornando-o tão legível quanto síncrono. Primeiro aprendizado: sempre prefira async/await sobre callbacks e evite o excesso de .then() — o código fica mais limpo e fácil de manter. Segundo aprendizado: lembre-se que await pausa a execução, então use Promise.all() quando operações são independentes para não desperdiçar performance. Terceiro aprendizado: dominando loops assíncrono e tratamento de erros com try/catch, você terá total controle sobre fluxos complexos de dados e requisições.