DEV Community

Cover image for Async/await em JavaScript: o que acontece de verdade quando você dá `await`
Erick Willian
Erick Willian

Posted on

Async/await em JavaScript: o que acontece de verdade quando você dá `await`

Todo dev usa. Quase ninguém entende o que está rolando. Esse artigo conserta isso.


Por que isso importa atualmente

Você vai escrever (ou a IA) async e await umas 50 vezes por dia. Em qualquer projeto Node, qualquer componente React que busca dado, qualquer endpoint de API.

E aí o bug aparece. A função demora 3 segundos quando devia demorar 300ms. O for com await dentro é o gargalo. Aparece um "unhandled promise rejection" no log que ninguém entende. O React reclama de "state update on unmounted component".

A questão é: assíncrono em JS não é só sintaxe. É um modelo mental. Quem só decorou a sintaxe trava na primeira complicação. Quem entende o modelo nunca mais escreve código lento sem perceber.

Bora desmontar.


A analogia

Imagina uma cozinha de restaurante. Tem um chef (a thread principal do JS). Ele faz uma coisa de cada vez — ele é single-threaded.

Mas tem várias coisas que demoram: assar bolo, ferver água, marinar carne. Se o chef ficasse parado esperando o bolo assar, o restaurante quebrava.

Então ele faz o seguinte: bota o bolo no forno e fala pra alguém avisar quando ficar pronto. Enquanto isso, vai cortar cebola, atender pedido, fazer outra coisa. Quando o forno bipa ("o bolo tá pronto"), ele larga o que está fazendo e tira o bolo.

Isso é o JavaScript com async/await. O chef é a engine do JS. O forno é uma operação externa (I/O, rede, timer). O "alguém avisar" é o event loop.

JS não é multithread (na maioria dos casos). Ele é concorrente, não paralelo. Ele faz uma coisa de cada vez, mas é muito esperto em não ficar parado esperando.


O que é uma Promise, na real

Promise é só um objeto. Um objeto que representa um valor que ainda não existe, mas vai existir (ou vai falhar) no futuro.

Ela tem 3 estados:

  • pending — esperando
  • fulfilled — deu certo, tem valor
  • rejected — deu errado, tem erro
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Pronto!"); // a Promise muda pra fulfilled depois de 1s
  }, 1000);
});

console.log(promise); // Promise { <pending> }
Enter fullscreen mode Exit fullscreen mode

Antes de async/await existir, a gente usava .then():

promise
  .then(value => console.log(value))
  .catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode

async/await é sintaxe em cima de Promise. Mais nada. Não é uma feature nova de runtime. É só açúcar.


O que async faz com a sua função

Quando você bota async na frente de uma função, três coisas mudam:

  1. A função sempre retorna uma Promise, mesmo que você retorne um número
  2. Dentro dela, você pode usar await
  3. Erros viram Promise rejeitada automaticamente
async function soma(a, b) {
  return a + b;
}

const resultado = soma(2, 3);
console.log(resultado); // Promise { 5 } — NÃO é 5!

// pra pegar o valor real:
soma(2, 3).then(v => console.log(v)); // 5
// ou:
const v = await soma(2, 3); // dentro de outra async function
Enter fullscreen mode Exit fullscreen mode

Sacar isso é fundamental. Toda função async retorna Promise. Sempre.


O que await faz por baixo dos panos

Aqui é onde a maioria erra. Quando você escreve:

async function buscarUsuario(id) {
  const resp = await fetch(`/api/users/${id}`);
  const user = await resp.json();
  return user;
}
Enter fullscreen mode Exit fullscreen mode

O que o motor do JS faz mecanicamente:

  1. Chega no primeiro await
  2. Pausa a função e devolve uma Promise pra quem chamou
  3. Bota uma "marcação" no event loop: "quando essa Promise resolver, volta aqui e continua"
  4. Vai fazer outras coisas (event loop continua rodando)
  5. Quando fetch resolve, o event loop chama a função de novo, do ponto onde parou
  6. Atribui o valor a resp, segue pra próxima linha
  7. Outro await, repete o ciclo

A função não fica esperando. Ela é suspensa e retomada. Isso é assíncrono.

Bug clássico que vem disso:

// LENTO — espera cada uma, sequencial
async function carregarTudo() {
  const usuarios = await fetchUsuarios();    // 200ms
  const produtos = await fetchProdutos();    // 200ms
  const pedidos = await fetchPedidos();      // 200ms
  return { usuarios, produtos, pedidos };
}
// Total: 600ms
Enter fullscreen mode Exit fullscreen mode
// RÁPIDO — dispara todos juntos, espera todos
async function carregarTudo() {
  const [usuarios, produtos, pedidos] = await Promise.all([
    fetchUsuarios(),
    fetchProdutos(),
    fetchPedidos()
  ]);
  return { usuarios, produtos, pedidos };
}
// Total: 200ms (o tempo do mais lento)
Enter fullscreen mode Exit fullscreen mode

Se as 3 chamadas não dependem uma da outra, use Promise.all. Toda vez que você vê await ... await ... await em sequência, é candidato a refactor.


O bug que mais aparece: await dentro de forEach

Esse aqui pega geral.

// NÃO FUNCIONA como você espera
async function processar(itens) {
  itens.forEach(async (item) => {
    await processarItem(item);
  });
  console.log("Acabei!"); // imprime ANTES de qualquer item terminar
}
Enter fullscreen mode Exit fullscreen mode

forEach não espera Promise. Ele dispara todas as callbacks e segue em frente. Se você quer sequencial:

// SEQUENCIAL — um de cada vez
async function processar(itens) {
  for (const item of itens) {
    await processarItem(item);
  }
  console.log("Acabei!"); // só imprime depois de TODOS
}
Enter fullscreen mode Exit fullscreen mode

Se você quer paralelo:

// PARALELO — todos juntos
async function processar(itens) {
  await Promise.all(itens.map(item => processarItem(item)));
  console.log("Acabei!");
}
Enter fullscreen mode Exit fullscreen mode

Regra: forEach e callbacks de array (map, filter) não combinam com await quando você precisa esperar.


Error handling — o lado que ninguém ensina

Erro em código assíncrono é traiçoeiro. Promise rejeitada que ninguém pega vira "unhandled promise rejection" — em algumas runtimes, derruba o processo.

// ERRADO — erro fica sem ser pego
async function buscar() {
  fetch("/api/x"); // sem await, sem .catch
}
Enter fullscreen mode Exit fullscreen mode
// CERTO — try/catch funciona em async/await
async function buscar() {
  try {
    const resp = await fetch("/api/x");
    return await resp.json();
  } catch (err) {
    console.error("Falhou:", err);
    throw err; // ou trate, depende do caso
  }
}
Enter fullscreen mode Exit fullscreen mode

Regra prática: toda função async deve ter try/catch ou o erro deve ser tratado por quem chamou. Não existe "deixa o erro ir embora" — em assíncrono ele explode em lugar inesperado.


O event loop em 4 linhas

Pra fechar o modelo mental, isso aqui é o event loop simplificado:

1. Pega a próxima task da fila (call stack)
2. Executa até bater num await/setTimeout/I/O
3. Bota a continuação na fila de "callbacks pendentes"
4. Volta pro passo 1
Enter fullscreen mode Exit fullscreen mode

JS roda uma coisa de cada vez no event loop. Mas como ele suspende e retoma rapidamente, parece que está fazendo várias.

Quando alguém te perguntar "por que JS é single-threaded e ainda assim rápido?", essa é a resposta: porque ele não fica parado.


Conclusão prática

Recap do que importa:

  1. Promise é objeto. Tem 3 estados. Async/await é açúcar em cima.
  2. async sempre retorna Promise. Sempre.
  3. await pausa a função, não a thread. O event loop continua.
  4. Sequencial vs paralelo: se não há dependência, use Promise.all.
  5. forEach não espera. Use for...of (sequencial) ou Promise.all (paralelo).
  6. Erro precisa ser pego. Try/catch em async é como try/catch normal.

Com isso na cabeça, você nunca mais vai:

  • Escrever 3 awaits em sequência sem perceber que dá pra paralelizar
  • Cair no bug do forEach
  • Ver "unhandled promise rejection" e não saber de onde veio
  • Achar que JS é "lento porque é single-threaded"

Próximo passo prático: pega um código teu (de back ou front), procura por await em loop. Aposto uma pizza que tem pelo menos um lugar onde dá pra usar Promise.all e cortar tempo de resposta pela metade.

Quer que eu vá fundo em streams, AsyncIterator ou Web Workers no próximo? Comenta aí.

Me segue no LinkedIn.

Faz sentido? Qual desses bugs você já viu rodando em produção?


Top comments (0)