async/await é só Promise com outra roupagem
O async/await não é um novo modelo de concorrência. É só um açúcar sintático em cima das Promises que deixa você escrever código que parece sequencial, mesmo sendo assíncrono. A engrenagem é a mesma, só que bem mais agradável de ler.
Veja a mesma tarefa escrita das duas formas:
As duas funções retornam uma Promise e fazem exatamente a mesma coisa. A diferença é que a versão com async pode ser lida de cima para baixo, sem aquela corrente de .then — e é justamente aí que mora o charme.
async sinaliza que a função retorna uma Promise
Ao colocar async antes de uma function, de uma arrow function ou de um método, duas coisas acontecem:
- A função sempre retorna uma Promise. O que você passar no
returnvira o valor resolvido. - Você libera o uso do
awaitdentro dela.
Perceba que result não é a string em si — é uma promise que resolve para aquela string. Mesmo que greet não tenha nenhum await nem qualquer operação assíncrona, a palavra-chave async ainda envolve o valor de retorno em uma promise. E se a função lançar um erro, a promise é rejeitada.
await: pausando até a promise resolver
Dentro de uma função async, usar await umaPromise pausa a execução daquela função até que a promise resolva, e então devolve o valor resolvido. Caso a promise seja rejeitada, o await lança a exceção.
Preste atenção na ordem do output. O "started countdown" aparece antes do "2" — isso porque await só pausa a função async, não o resto do programa. O event loop continua rodando; a countdown só volta a executar quando cada promise de wait resolve.
Dá pra usar await em qualquer coisa parecida com uma promise. Até await 42 funciona — valores que não são promises são envolvidos automaticamente em Promise.resolve(42) e resolvidos na hora.
Tratamento de erro com try/catch no async/await
Com promises puras, você encadeia .catch(). Já com async/await, uma promise rejeitada vira uma exceção que você captura do jeito tradicional:
Um único try/catch cobre todos os await dentro dele. Falhas de rede, erros ao parsear JSON e os seus próprios throw caem todos no mesmo catch. É um ganho real em relação àquelas cadeias aninhadas de .then/.catch.
Só fique atento a um detalhe: o fetch só rejeita em erros de rede, não em respostas HTTP 4xx/5xx. Você mesmo precisa verificar res.ok e disparar o erro — um padrão que vai aparecer o tempo todo em código de verdade.
Evite usar await dentro de loop em JavaScript sem necessidade
Essa é a pegadinha mais comum quando o assunto é async/await. Usar await sequencial dentro de um loop faz cada iteração esperar a anterior terminar:
sequential leva cerca de 900ms. Já parallel leva uns 300ms. A regra prática é simples: se as tarefas não dependem do resultado umas das outras, dispare todas e depois use await Promise.all. Só faça await uma a uma quando a próxima chamada realmente precisar do resultado da anterior.
Para coleções, o idiomático é Promise.all(items.map(async (x) => ...)). Um for...of comum com await dentro roda em série — às vezes é o que você quer (para respeitar rate limit ou manter ordem), mas na maioria dos casos não é.
Misturando async/await com Promises puras
Você não precisa escolher um lado. Funções async retornam promises, e o await funciona com qualquer promise — então dá para misturar os dois à vontade:
Os dois estilos são intercambiáveis. Use await quando o código fluir melhor de cima para baixo; use .then quando quiser algo rápido e pontual ou quando estiver fora de um contexto async.
Top-level await (em ES Modules)
Antigamente, era obrigatório envolver o await dentro de uma função async, porque o await não podia aparecer no nível superior do script. Isso mudou: dentro de um ES module (um arquivo .mjs ou um <script type="module">), agora dá para usar await direto no topo do arquivo:
// em um módulo ES
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await res.json();
console.log(user.name);
O await no nível superior (top level await) adia a finalização do módulo até que a promise aguardada seja resolvida — qualquer módulo que importar esse arquivo também vai esperar. É útil para carregar configurações e fazer imports dinâmicos, mas use com parcimônia: um await lento no topo trava todo mundo que importar aquele módulo.
Em arquivos CommonJS ou em scripts inline comuns, isso continua gerando SyntaxError. A saída clássica é usar uma função async autoinvocada:
Pegadinhas que pegam todo mundo
Um tour rápido pelas ciladas mais comuns com async/await:
- Esquecer o
async. Usarawaitdentro de uma função normal gera erro de sintaxe. A solução é adicionarasyncna função — ou chamar a função assíncrona usando.then. - Esquecer de dar
awaitno resultado.const data = getJSON(url);te devolve uma promise, não o dado em si. Se você usar esse valor como se fosse a resposta pronta, vai acabar vendo[object Promise]aparecendo na saída. - Rejeições não tratadas. Uma função async disparada sem acompanhamento (
doWork();) vai engolir os erros em silêncio, a não ser que você encadeie um.catchou useawaitdentro de umtry/catch. forEachcom callback async.array.forEach(async (x) => await something(x))não espera nada — oforEachsimplesmente ignora as promises retornadas. Prefirafor...ofcomawait, ou entãoPromise.all(array.map(...)).
Execute: "finalizado?" aparece antes de qualquer "concluído", porque broken retorna sem esperar. Já fixed aguarda tudo e só então imprime "finalizado!" por último.
Quando usar async/await
Use async/await por padrão sempre que seu código tiver mais de um passo assíncrono em sequência, ou quando precisar de tratamento de erro no estilo try/catch. Promises puras ainda fazem sentido em one-liners triviais, em código de biblioteca que só devolve uma promise sem precisar aguardar nada, ou quando você realmente precisa de combinadores como Promise.race ou um .finally() encadeado.
Bem aplicado, o async/await faz o código assíncrono parecer uma receita de bolo: faz isso, depois isso, depois aquilo. O event loop continua trabalhando nos bastidores — você só não precisa mais raciocinar em termos de callbacks.
A seguir: a API fetch
A maior parte dos exemplos aqui usou fetch como um substituto genérico para "alguma coisa assíncrona". Vale olhar com mais calma: como funcionam requisições e respostas, como lidar com JSON, como definir headers e por que o fetch não rejeita em erros HTTP. É o que veremos na próxima página.
Perguntas frequentes
O que async/await faz em JavaScript?
async/await é uma sintaxe para lidar com promises que permite escrever código assíncrono como se fosse síncrono. O async marca uma função como retornando uma promise, e o await pausa a execução dentro dessa função até a promise ser resolvida, devolvendo o valor resultante. Por baixo dos panos continua sendo tudo promise — só que bem mais legível.
Dá para usar await fora de uma função async?
No topo de um módulo ES, sim — é o chamado top-level await. Já dentro de funções comuns ou em scripts CommonJS, não: usar await fora de uma função async gera erro de sintaxe. A solução costuma ser envolver o trecho numa função async e chamá-la, ou converter o arquivo para módulo ES.
Como tratar erros com async/await?
Envolva as chamadas com await em um bloco try/catch. Qualquer promise rejeitada que você aguardar com await vira uma exceção que cai no catch. Para tarefas em segundo plano que você não aguarda, lembre-se de anexar um .catch() à promise retornada para não deixar rejeições sem tratamento.
O await trava o programa inteiro?
Não. O await só pausa a função async atual. O event loop continua rodando normalmente — timers disparam, outras tarefas assíncronas progridem e a UI segue responsiva. Quem chamou a função recebe uma promise pendente na hora e continua sua execução.