As Promises resolveram o Callback Hell. Mas encadear muitos .then() ainda pode ficar confuso quando há lógica condicional, loops ou múltiplas variáveis que precisam ser compartilhadas entre etapas.
O async/await, introduzido no ES2017, é açúcar sintático em cima das Promises — ele não substitui as Promises, apenas fornece uma sintaxe mais limpa para trabalhar com elas. O resultado é código assíncrono que se lê como código síncrono, sem perder nenhum dos benefícios das Promises.
A palavra-chave async
async transforma uma função em uma função assíncrona. Uma função async sempre retorna uma Promise, mesmo que você não use return explicitamente ou retorne um valor simples:
// Função síncrona comum
function somar(a, b) {
return a + b;
}
console.log(somar(2, 3)); // 5
// Função assíncrona — retorna uma Promise
async function somarAsync(a, b) {
return a + b;
}
somarAsync(2, 3).then(resultado => console.log(resultado)); // 5
// Também funciona com arrow functions
const multiplicar = async (a, b) => a * b;
A palavra-chave await
await só pode ser usado dentro de funções async. Ele pausa a execução da função até que a Promise seja resolvida — e retorna o valor resolvido:
function esperar(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function exemplo() {
console.log("Início");
await esperar(2000); // pausa aqui por 2 segundos
console.log("2 segundos depois");
await esperar(1000); // pausa mais 1 segundo
console.log("Mais 1 segundo depois");
}
exemplo();
console.log("Este executa imediatamente — não espera a função async");
// Saída:
// Início
// Este executa imediatamente — não espera a função async
// (2 segundos)
// 2 segundos depois
// (1 segundo)
// Mais 1 segundo depois
O await pausa somente a função async atual — o restante do programa continua normalmente.
Comparando Promises com async/await
O mesmo código escrito das duas formas:
function buscarUsuario(id) {
return new Promise(resolve =>
setTimeout(() => resolve({ id, nome: "Ana", plano: "premium" }), 800)
);
}
function buscarPedidos(usuarioId) {
return new Promise(resolve =>
setTimeout(() => resolve([{ id: 101 }, { id: 102 }]), 600)
);
}
// ── Com Promises e .then() ──────────────────
function carregarDadosPromise() {
return buscarUsuario(1)
.then(usuario => {
console.log(`Usuário: ${usuario.nome}`);
return buscarPedidos(usuario.id);
})
.then(pedidos => {
console.log(`Pedidos: ${pedidos.length}`);
})
.catch(erro => console.error(erro.message));
}
// ── Com async/await ─────────────────────────
async function carregarDadosAsync() {
try {
const usuario = await buscarUsuario(1);
console.log(`Usuário: ${usuario.nome}`);
const pedidos = await buscarPedidos(usuario.id);
console.log(`Pedidos: ${pedidos.length}`);
} catch (erro) {
console.error(erro.message);
}
}
A versão com async/await é mais legível — especialmente quando há lógica condicional entre as etapas.
Tratamento de erros com try/catch
Com async/await, o tratamento de erros volta a usar o familiar try/catch:
async function processarPedido(usuarioId, produtoId) {
try {
// Cada await pode lançar um erro se a Promise rejeitar
const usuario = await buscarUsuario(usuarioId);
const produto = await buscarProduto(produtoId);
if (produto.estoque === 0) {
throw new Error(`Produto "${produto.nome}" sem estoque.`);
}
const pedido = await criarPedido(usuario.id, produto.id);
console.log(`✅ Pedido #${pedido.id} criado com sucesso!`);
return pedido;
} catch (erro) {
console.error(`❌ Erro ao processar pedido: ${erro.message}`);
throw erro; // repropaga se necessário
} finally {
console.log("Processo de pedido finalizado.");
}
}
Lógica condicional — onde async/await brilha
O ponto onde async/await supera claramente o encadeamento de .then():
async function checkout(carrinho, cupom) {
const usuario = await buscarUsuarioLogado();
// Lógica condicional — muito mais clara que em .then()
if (!usuario) {
throw new Error("Usuário não autenticado.");
}
const itensVerificados = await verificarEstoque(carrinho);
let total = itensVerificados.reduce((acc, item) => acc + item.preco * item.quantidade, 0);
if (cupom) {
try {
const desconto = await validarCupom(cupom, total);
total = total - desconto;
console.log(`Cupom aplicado! Desconto: R$ ${desconto}`);
} catch {
console.warn("Cupom inválido ou expirado. Continuando sem desconto.");
}
}
const frete = await calcularFrete(usuario.cep);
total += frete.valor;
console.log(`Total com frete: R$ ${total.toFixed(2)}`);
console.log(`Previsão de entrega: ${frete.prazo}`);
const pedido = await confirmarPedido({ itens: itensVerificados, total, usuario });
return pedido;
}
Tente reescrever isso com .then() encadeados e você vai ver o quanto async/await ajuda.
Loops assíncronos
Async/await permite usar estruturas de controle normais com código assíncrono:
const ids = [1, 2, 3, 4, 5];
// ── Sequencial — aguarda cada um antes do próximo
async function carregarSequencial() {
console.log("Carregando em sequência...");
const resultados = [];
for (const id of ids) {
const usuario = await buscarUsuario(id); // aguarda antes de continuar
resultados.push(usuario);
console.log(`Carregado: ${usuario.nome}`);
}
return resultados;
}
// Tempo total: soma dos tempos individuais (~5s se cada um levar 1s)
// ── Paralelo — inicia todos ao mesmo tempo
async function carregarParalelo() {
console.log("Carregando em paralelo...");
const promises = ids.map(id => buscarUsuario(id));
const resultados = await Promise.all(promises);
resultados.forEach(u => console.log(`Carregado: ${u.nome}`));
return resultados;
}
// Tempo total: tempo do mais lento (~1s)
Use sequencial quando cada operação depende da anterior. Use paralelo (Promise.all) quando as operações são independentes.
async/await com Promise.all
Você pode — e deve — combinar async/await com os métodos de Promise:
async function carregarDashboard(usuarioId) {
// Inicia tudo em paralelo — eficiente
const [usuario, pedidos, notificacoes, relatorio] = await Promise.all([
buscarUsuario(usuarioId),
buscarPedidos(usuarioId),
buscarNotificacoes(usuarioId),
gerarRelatorio(usuarioId),
]);
return { usuario, pedidos, notificacoes, relatorio };
}
// Usando
async function main() {
try {
const dados = await carregarDashboard(42);
console.log(`Bem-vindo, ${dados.usuario.nome}!`);
console.log(`${dados.pedidos.length} pedidos encontrados.`);
console.log(`${dados.notificacoes.length} notificações.`);
} catch (erro) {
console.error("Erro ao carregar dashboard:", erro.message);
}
}
main();
Top-level await
No passado, await só podia ser usado dentro de funções async. Em módulos JavaScript modernos (.mjs ou com "type": "module" no package.json), você pode usar await diretamente no nível do módulo:
// Em um módulo ES moderno — sem precisar de função async
const dados = await fetch("https://api.exemplo.com/dados");
const json = await dados.json();
console.log(json);
Isso é muito útil em scripts de configuração e no Node.js moderno.
Erro comum — await em forEach
Um dos erros mais frequentes com async/await: usar await dentro de forEach não funciona como esperado:
const ids = [1, 2, 3];
// ❌ ERRADO — forEach não aguarda os awaits
async function errado() {
ids.forEach(async (id) => {
const usuario = await buscarUsuario(id);
console.log(usuario.nome); // a ordem é imprevisível
});
console.log("Terminou?"); // executa ANTES dos awaits!
}
// ✅ CORRETO — use for...of para código sequencial
async function correto() {
for (const id of ids) {
const usuario = await buscarUsuario(id);
console.log(usuario.nome); // ordem garantida
}
console.log("Terminou!"); // executa DEPOIS de todos
}
// ✅ CORRETO — use Promise.all para paralelo
async function paraleloCorreto() {
const usuarios = await Promise.all(ids.map(id => buscarUsuario(id)));
usuarios.forEach(u => console.log(u.nome));
console.log("Terminou!");
}
Padrões avançados
Retry automático — tentar novamente em caso de falha:
async function comRetry(funcao, tentativas = 3, delay = 1000) {
for (let i = 1; i <= tentativas; i++) {
try {
return await funcao();
} catch (erro) {
console.warn(`Tentativa ${i}/${tentativas} falhou: ${erro.message}`);
if (i === tentativas) throw erro; // última tentativa — propaga o erro
await new Promise(resolve => setTimeout(resolve, delay * i)); // espera antes de tentar de novo
}
}
}
// Uso
const dados = await comRetry(
() => buscarDadosDaAPI(),
3, // 3 tentativas
500, // 500ms, 1000ms, 1500ms entre tentativas
);
Timeout em requisições:
async function comTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout após ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// Uso
try {
const dados = await comTimeout(buscarDadosDaAPI(), 5000);
console.log(dados);
} catch (erro) {
console.error(erro.message); // "Timeout após 5000ms"
}
Exemplo completo — sistema de autenticação
// Simulando serviços
const authService = {
async validarCredenciais(email, senha) {
await new Promise(r => setTimeout(r, 800));
if (email === "admin@email.com" && senha === "senha123") {
return { token: "jwt_token_abc123", expiraEm: 3600 };
}
throw new Error("Credenciais inválidas.");
},
async obterPerfil(token) {
await new Promise(r => setTimeout(r, 500));
if (!token) throw new Error("Token ausente.");
return {
id: 1,
nome: "Administrador",
email: "admin@email.com",
permissoes: ["ler", "escrever", "deletar"],
};
},
async registrarAcesso(userId) {
await new Promise(r => setTimeout(r, 200));
const agora = new Date().toLocaleString("pt-BR");
return { userId, acessoEm: agora, ip: "192.168.1.1" };
},
};
// Função de login completa
async function login(email, senha) {
console.log("🔐 Iniciando autenticação...");
try {
// Passo 1 — valida credenciais
const auth = await authService.validarCredenciais(email, senha);
console.log(`✅ Credenciais válidas. Token expira em ${auth.expiraEm}s`);
// Passo 2 e 3 — busca perfil e registra acesso em paralelo
const [perfil, registro] = await Promise.all([
authService.obterPerfil(auth.token),
authService.registrarAcesso(1),
]);
console.log(`👤 Bem-vindo, ${perfil.nome}!`);
console.log(`🔑 Permissões: ${perfil.permissoes.join(", ")}`);
console.log(`📋 Acesso registrado em: ${registro.acessoEm}`);
return { auth, perfil, registro };
} catch (erro) {
console.error(`❌ Falha no login: ${erro.message}`);
throw erro;
}
}
async function main() {
// Login bem-sucedido
await login("admin@email.com", "senha123");
console.log("\n--- Tentativa com credenciais erradas ---\n");
// Login com falha
await login("hacker@email.com", "errada").catch(() => {
console.log("Redirecionando para tela de erro...");
});
}
main();
Resumo — quando usar cada abordagem
| Situação | Use |
|---|---|
| Código simples e linear | async/await |
| Operações em paralelo | async/await + Promise.all |
| Encadeamento simples | .then() ou async/await |
| Eventos que ocorrem várias vezes | Callbacks |
Precisa de .race(), .any(), .allSettled() |
Combine com await |
| Loop que precisa ser sequencial | for...of com await |
| Loop paralelo | Promise.all + map com await |
Tarefa para você
Construa um sistema de importação de dados em lotes com async/await:
// Contexto: você tem 50 usuários para importar para um banco de dados.
// Importar todos de uma vez pode sobrecarregar o servidor.
// Importe em lotes de 10, com 500ms de pausa entre os lotes.
async function importarEmLotes(usuarios, tamanhoDeLote = 10, pausaMs = 500) {
// 1. Divida o array em lotes de 'tamanhoDeLote'
// 2. Para cada lote, importe todos em paralelo (Promise.all)
// 3. Aguarde 'pausaMs' antes do próximo lote
// 4. Exiba o progresso: "Lote 1/5 concluído (10/50 usuários)"
// 5. Retorne um relatório final: { sucesso, falha, total }
}
// Simule a função de importar um usuário:
async function importarUsuario(usuario) {
await new Promise(r => setTimeout(r, Math.random() * 300 + 100));
if (Math.random() < 0.1) throw new Error(`Falha ao importar ${usuario.nome}`);
return { id: Math.random(), ...usuario };
}
Ver solução — importarEmLotes com relatório, sem perder o lote por uma falha
// A função simulada do enunciado, com semente fixa para o resultado
// ser reproduzível ao rodar o exemplo.
async function importarUsuario(usuario) {
await new Promise((r) => setTimeout(r, Math.random() * 300 + 100));
if (Math.random() < 0.1) {
throw new Error(`Falha ao importar ${usuario.nome}`);
}
return { id: Math.random(), ...usuario };
}
const esperar = (ms) => new Promise((resolver) => setTimeout(resolver, ms));
// ---------------------------------------------------------------
// 1 — dividir em lotes
// ---------------------------------------------------------------
function emLotes(itens, tamanho) {
const lotes = [];
for (let i = 0; i < itens.length; i += tamanho) {
lotes.push(itens.slice(i, i + tamanho));
}
return lotes;
}
// ---------------------------------------------------------------
// 2 a 5 — a importação
// ---------------------------------------------------------------
async function importarEmLotes(usuarios, tamanhoDeLote = 10, pausaMs = 500) {
const lotes = emLotes(usuarios, tamanhoDeLote);
const sucesso = [];
const falha = [];
for (const [indice, lote] of lotes.entries()) {
// allSettled, não all: `Promise.all` rejeita no primeiro erro e
// descarta o resultado dos outros nove, que JÁ FORAM importados.
// O banco ficaria com registros que o relatório não conhece.
const resultados = await Promise.allSettled(lote.map(importarUsuario));
resultados.forEach((resultado, i) => {
if (resultado.status === "fulfilled") {
sucesso.push(resultado.value);
} else {
falha.push({
usuario: lote[i],
motivo: resultado.reason.message,
});
}
});
const processados = sucesso.length + falha.length;
console.log(
`Lote ${indice + 1}/${lotes.length} concluído ` +
`(${processados}/${usuarios.length} usuários)`
);
// Sem pausa depois do último lote: seriam 500 ms de espera à toa
// antes de devolver o relatório.
if (indice < lotes.length - 1) await esperar(pausaMs);
}
return {
sucesso: sucesso.length,
falha: falha.length,
total: usuarios.length,
importados: sucesso,
erros: falha,
};
}
// ---------------------------------------------------------------
// Rodando
// ---------------------------------------------------------------
const usuarios = Array.from({ length: 50 }, (_, i) => ({
nome: `Usuário ${String(i + 1).padStart(2, "0")}`,
email: `usuario${i + 1}@exemplo.com`,
}));
const relatorio = await importarEmLotes(usuarios);
console.log(`\n${relatorio.sucesso} importados · ${relatorio.falha} falharam ` +
`· ${relatorio.total} no total`);
relatorio.erros.forEach((e) => console.log(` ❌ ${e.usuario.nome}: ${e.motivo}`));
// ---------------------------------------------------------------
// Por que o for...of e não um forEach
// ---------------------------------------------------------------
// `await` dentro de forEach não espera nada: o callback é async, o
// forEach ignora a promise devolvida e passa para o próximo na hora.
// Os 5 lotes disparariam juntos — exatamente o que o exercício quer
// evitar.
//
// lotes.forEach(async (lote) => {
// await Promise.allSettled(lote.map(importarUsuario)); // ❌ ninguém espera
// });
//
// `for...of` com await pausa o laço de verdade. É o jeito de fazer
// trabalho assíncrono em SÉRIE.
// ---------------------------------------------------------------
// Uma alternativa melhor que lote fixo: pool com concorrência
// ---------------------------------------------------------------
// Lote fixo tem um problema: o lote inteiro espera pelo item mais
// lento antes de o próximo começar. Um pool mantém sempre N em voo.
async function comPool(itens, limite, tarefa) {
const resultados = new Array(itens.length);
let proximo = 0;
async function trabalhador() {
while (proximo < itens.length) {
const indice = proximo++; // captura antes do await, senão dois pegam o mesmo
try {
resultados[indice] = { status: "ok", valor: await tarefa(itens[indice]) };
} catch (erro) {
resultados[indice] = { status: "erro", motivo: erro.message };
}
}
}
await Promise.all(Array.from({ length: limite }, trabalhador));
return resultados;
}
const doPool = await comPool(usuarios.slice(0, 20), 5, importarUsuario);
console.log(`\nPool: ${doPool.filter((r) => r.status === "ok").length}/20 importados`);
Promise.all rejeita no primeiro erro — e as outras promessas continuam rodando, com o resultado descartado. Numa importação isso deixa registros gravados que o relatório não conhece. Quando você quer saber o que aconteceu com cada item, é allSettled.
async e await são açúcar sobre Promise, e lembrar disso evita os dois enganos mais comuns: uma função async devolve uma Promise mesmo quando o corpo é inteiramente síncrono, e o await só espera aquilo que ele próprio alcança — dentro de um forEach, não espera nada. Para operações independentes, a forma continua sendo Promise.all; esperar uma de cada vez só se justifica quando a chamada seguinte precisa do resultado da anterior.
Fontes e Referências
- MDN Web Docs — async function: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Statements/async_function
- MDN Web Docs — await: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Operators/await
- MDN Web Docs — Top-level await: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#top_level_await
- JavaScript.info — Async/await: https://javascript.info/async-await
- JavaScript.info — Promise API: https://javascript.info/promise-api
- Google Developers — JavaScript Promises: https://web.dev/promises
Exercícios
Exercício 1
O map tem callback async e não há Promise.all. O que sai em A e em B — e alguma das duas lança erro?
const ids = [1, 2, 3];
async function carregar() {
const usuarios = ids.map(async (id) => await buscarUsuario(id));
console.log(usuarios.length); // A
console.log(usuarios[0].nome); // B
}
Ver resposta
✓ Resposta: A imprime 3 e B imprime undefined — e nenhuma das duas lança erro, que é justamente o que torna o defeito difícil de achar. O map não sabe nada de assincronia: ele chama o callback e guarda o que ele devolver, e um callback async devolve sempre uma Promise. Então usuarios é um array de três promises, com length igual a 3, e usuarios[0] é um objeto Promise que simplesmente não tem a propriedade nome — acessar propriedade inexistente devolve undefined em silêncio, sem exceção nenhuma. A correção é envolver o array num await Promise.all(...), que espera as três e devolve os valores. Repare ainda numa redundância no próprio callback: async (id) => await buscarUsuario(id) tem um await que não serve para nada, já que a função async devolveria a promise de qualquer forma — ids.map(id => buscarUsuario(id)), ou simplesmente ids.map(buscarUsuario), produz exatamente o mesmo array.
Exercício 2
Cada busca leva 1 segundo. Quanto tempo leva cada um dos três blocos?
// A
const a = await buscarUsuario(1);
const b = await buscarUsuario(2);
// B
const [c, d] = await Promise.all([buscarUsuario(1), buscarUsuario(2)]);
// C
const p1 = buscarUsuario(1);
const p2 = buscarUsuario(2);
const e = await p1;
const f = await p2;
Ver resposta
✓ Resposta: A leva 2 segundos; B e C levam 1 segundo cada. O C é o que costuma surpreender, e ele revela o ponto central: await não inicia coisa nenhuma. A operação começa no instante em que buscarUsuario(1) é chamada — a promise já está correndo antes de qualquer await. Em C as duas partem juntas, e os dois await apenas colhem resultados que chegam praticamente ao mesmo tempo; quando o segundo é alcançado, a promise dele já está resolvida ou quase. Em A, ao contrário, a segunda chamada só acontece depois de o primeiro await devolver, e por isso os tempos se somam. Ou seja, a diferença entre paralelo e sequencial não está no await, está em onde a chamada é feita. Vale uma ressalva prática sobre C: se p2 rejeitar antes de o código chegar ao await p2, há um intervalo em que essa rejeição não tem observador, e alguns ambientes já disparam o aviso de rejeição não tratada — motivo pelo qual Promise.all é a forma preferida.
Exercício 3
buscarUsuario(-1) sempre rejeita. Qual das duas funções captura o erro?
async function A() {
try {
buscarUsuario(-1);
} catch (erro) {
console.log("A capturou");
}
}
async function B() {
try {
return buscarUsuario(-1);
} catch (erro) {
console.log("B capturou");
}
}
Ver resposta
✓ Resposta: Nenhuma das duas. Em A falta o await: a chamada apenas cria a promise e devolve na hora, o bloco try termina com sucesso, e a rejeição chega depois, quando o catch já não está mais no caminho — o resultado é um UnhandledPromiseRejection. Em B a armadilha é mais fina: o return devolve a promise para quem chamou antes de ela se assentar, então a função sai do try imediatamente e a rejeição vira problema do chamador, não deste catch. As duas se consertam com a mesma palavra: await buscarUsuario(-1) e return await buscarUsuario(-1). Esse é exatamente o caso em que return await não é redundante, apesar de os linters costumarem sinalizá-lo como tal — dentro de um try, é ele que mantém a função presente para capturar a falha. A regra que resume tudo: try/catch só enxerga aquilo que este mesmo bloco esperou.
Exercício 4
A operação falha nas três tentativas. Quanto tempo o comRetry gasta esperando entre elas, e por que a última tentativa não espera?
async function comRetry(funcao, tentativas = 3, delay = 1000) {
for (let i = 1; i <= tentativas; i++) {
try {
return await funcao();
} catch (erro) {
if (i === tentativas) throw erro;
await new Promise(resolve => setTimeout(resolve, delay * i));
}
}
}
await comRetry(() => buscarDadosDaAPI(), 3, 500);
Ver resposta
✓ Resposta: Gasta 1500 ms em esperas: 500 ms depois da primeira falha e 1000 ms depois da segunda. A terceira não espera porque a linha if (i === tentativas) throw erro sai antes — e é assim que deve ser: esperar depois da última tentativa seria adiar a notificação do erro sem tentar nada. O tempo total, claro, ainda soma as três execuções da própria operação. Repare que delay * i produz um recuo linear (500, 1000, 1500…), não exponencial; a versão usada em produção costuma ser delay * 2 ** (i - 1), que cresce bem mais rápido e é mais gentil com um servidor em dificuldade. Falta ainda um detalhe importante nessa implementação: quando muitos clientes falham ao mesmo tempo — porque o servidor caiu —, todos recuam pelo mesmo intervalo e voltam juntos, dando uma nova pancada exatamente no momento em que ele tenta se recuperar. A correção é somar um valor aleatório ao intervalo, o chamado jitter. E vale a regra que antecede tudo isso: só faz sentido repetir operações idempotentes — repetir um pagamento pode cobrar duas vezes.
Exercício 5
Não há promise nenhuma aqui — o await recebe o número 42. Em que ordem as três linhas aparecem?
async function f() {
console.log("1");
const x = await 42;
console.log("2", x);
}
f();
console.log("3");
Ver resposta
✓ Resposta: Sai 1, 3, e só então 2 42. O await não verifica se o valor é uma promise para decidir se pausa: ele sempre suspende a função e agenda a continuação como microtask, envolvendo o valor comum numa promise já resolvida. Então await 42 devolve 42, sim, mas só na próxima volta do event loop — e tudo o que estiver depois da chamada de f(), no código síncrono, roda antes. É o mesmo mecanismo do artigo sobre o Event Loop, visto por dentro de uma função async: a parte antes do primeiro await é síncrona, e daí em diante são microtasks. A consequência prática aparece quando se acrescenta um await a uma função que não tinha nenhum: tudo o que vem depois dele passa a executar mais tarde, e código de fora que dependia da ordem antiga muda de comportamento sem que uma linha dele tenha sido tocada.