Promises e async/await: pare de se perder no assíncrono

Entenda o que uma Promise realmente representa, a diferença entre await em sequência e Promise.all em paralelo, e os erros mais comuns como esquecer o await ou deixar um erro sem tratamento.

tecnico javascript node 4 min de leitura

Nos posts sobre event loop, circuit breaker e fila de mensagem, usei async e await o tempo todo como se fossem óbvios. Pra muita gente não são, e o pior é que o código costuma funcionar o bastante pra esconder o mal-entendido, até o dia em que uma requisição fica lenta sem motivo ou um erro some sem deixar rastro.

O que é uma Promise

Uma Promise é um objeto que representa um valor que ainda não existe, mas vai existir (ou vai falhar) no futuro. Ela tem três estados: pendente, resolvida (com um valor) ou rejeitada (com um erro).

const promessa = fetch("https://api.exemplo.com/usuarios");
console.log(promessa); // Promise { <pending> }

Quando você chama fetch, a resposta não está pronta ainda. O que você recebe na hora é a Promise, uma espécie de recibo que diz "eu te entrego isso depois". O JavaScript segue executando o resto do código enquanto a rede trabalha, que é exatamente o comportamento do event loop que já expliquei aqui.

then e catch: o jeito antigo

Antes do async/await, o resultado era tratado encadeando funções:

fetch("/api/usuarios")
  .then((res) => res.json())
  .then((usuarios) => console.log(usuarios))
  .catch((erro) => console.error(erro));

Funciona, mas com várias operações dependentes o código vira uma escada de .then, difícil de ler e de depurar.

async/await: a mesma coisa, mais legível

async/await é açúcar sintático sobre Promises. Dentro de uma função async, o await pausa a execução daquela função até a Promise resolver, sem travar o resto do programa:

async function buscarUsuarios() {
  try {
    const res = await fetch("/api/usuarios");
    const usuarios = await res.json();
    console.log(usuarios);
  } catch (erro) {
    console.error(erro);
  }
}

O código lê de cima pra baixo, como se fosse síncrono, e o try/catch funciona como você já conhece. Só vale lembrar que toda função async devolve uma Promise, mesmo que você retorne um valor simples.

O erro de performance mais comum: await em sequência

async function carregarPagina() {
  const usuario = await buscarUsuario();
  const pedidos = await buscarPedidos();
  const produtos = await buscarProdutos();
  return { usuario, pedidos, produtos };
}

Se as três buscas não dependem uma da outra, esse código as executa uma depois da outra: se cada uma leva 300 ms, o total é 900 ms. A segunda só começa depois que a primeira termina, sem necessidade nenhuma.

async function carregarPagina() {
  const [usuario, pedidos, produtos] = await Promise.all([
    buscarUsuario(),
    buscarPedidos(),
    buscarProdutos(),
  ]);
  return { usuario, pedidos, produtos };
}

Com Promise.all, as três começam ao mesmo tempo e o total cai pra cerca de 300 ms, o tempo da mais lenta. A regra é simples: se uma operação depende do resultado da outra, use await em sequência. Se são independentes, dispare todas juntas.

Um cuidado: Promise.all rejeita assim que qualquer uma falha, descartando as outras. Quando você quer o resultado de todas, mesmo que algumas deem erro, existe o Promise.allSettled.

Os erros que mais aparecem

Esquecer o await. O código não quebra, ele só trabalha com a Promise em vez do valor:

const usuario = buscarUsuario(); // faltou o await
console.log(usuario.nome); // undefined, porque usuario é uma Promise

Usar await dentro de forEach. O forEach não espera as Promises, então o código seguinte roda antes de o trabalho terminar:

// não espera ninguém
itens.forEach(async (item) => {
  await salvar(item);
});
 
// espera uma por uma
for (const item of itens) {
  await salvar(item);
}
 
// espera todas, em paralelo
await Promise.all(itens.map((item) => salvar(item)));

Deixar a rejeição sem tratamento. Uma Promise rejeitada que ninguém captura vira um erro não tratado, que em versões recentes do Node derruba o processo inteiro. Toda chamada assíncrona precisa ter, em algum nível acima, um try/catch ou um .catch, como no post sobre tratamento de erro.

Fechando

Promise é a promessa de um valor futuro, async/await é a forma legível de esperar por ele, e Promise.all é o que separa um código que espera à toa de um que aproveita o tempo. Quase todo bug assíncrono vem de uma dessas três ideias mal encaixadas: esquecer de esperar, esperar em fila o que podia ir junto, ou esperar sem prever que pode falhar.

Direto na sua
caixa de entrada.

Um aviso por e-mail sempre que eu publicar um post novo. Sem spam, sem newsletter chata, só isso.