voltar

Async/Await: O que nenhum tutorial avisa

Passar três horas debugando um gargalo que não existia porque eu não entendia o que async/await realmente faz foi o que me fez repensar código assíncrono.

Tem uma diferença absurda entre código que simplesmente funciona e código que você realmente entende. Passei um bom tempo escrevendo o primeiro antes de finalmente chegar no segundo. A situação era comum: eu estava desenvolvendo uma nova feature no meu projeto e precisava buscar dados de duas APIs diferentes antes de processar e devolver a resposta para o frontend. A receita clássica do Node.js estava ali na minha frente, e eu achei que sabia exatamente o que fazer.

Eu tinha aprendido que o JavaScript roda em uma única thread e que operações de I/O não deveriam bloquear o restante da aplicação. A solução moderna para lidar com isso? async e await. O problema é que a maioria dos tutoriais te joga a sintaxe, diz que "resolve o problema do callback hell" e pronto, você que se vire para entender as implicações disso no mundo real. E foi exatamente isso que me pegou. Eu joguei await em cada chamada de banco de dados e em cada request para a API externa. O resultado? Um código que rodava mais devagar do que se eu tivesse feito de forma puramente síncrona, e um dev confuso tentando entender o gargalo.

O que aconteceu

Passar três horas debugando um race condition imaginário porque eu não entendia o que o async/await realmente faz por baixo dos panos foi exatamente o tipo de coisa que nenhum tutorial avisa. Eu coloquei await em tudo, literalmente um embaixo do outro, esperando que a famosa "mágica" do código assíncrono deixasse meu backend rápido e escalável. Na minha cabeça, se tinha async na frente da função, o Node iria paralelizar tudo sozinho.

Mas o código ficou lento. A cada refresh, a página demorava mais e mais para carregar. Eu cheguei a achar que o problema era minha conexão de internet, depois desconfiei que a API de terceiros estava limitando minhas requisições por segundo. Fui olhar os logs do banco de dados, adicionei dezenas de console.log para monitorar cada passo. Quando fui investigar de verdade usando o console.time(), vi que a primeira requisição levava 3 segundos e a segunda levava mais 2 segundos. O usuário estava esperando 5 segundos inteiros. E por quê? Porque eu estava obrigando o Node a esperar a primeira terminar para só então disparar a segunda requisição, de forma puramente sequencial.

Conceito que ficou claro

O que clicou para mim foi entender que o await realmente pausa a execução daquela função específica até a promise ser resolvida. Ele não pausa o programa inteiro (afinal, o event loop continua rodando), mas pausa a linha do tempo daquela função. E o mais importante: se uma requisição não depende do resultado da outra, eu não preciso, de forma alguma, esperar a primeira terminar para começar a disparar a segunda.

A analogia que funcionou perfeitamente pra mim foi a da lanchonete de fast-food. Pensa no fluxo: você não chega no caixa, pede o seu hambúrguer, espera ele ficar pronto, ser montado e entregue na sua bandeja, para só depois disso virar pro atendente e pedir o seu refrigerante. Se você fizer isso, vai demorar o dobro do tempo. O que você faz na vida real? Você pede o combo inteiro de uma vez. O caixa manda o pedido pra cozinha, e eles preparam a bebida e o sanduíche simultaneamente. Você só sai dali com a bandeja quando os dois estiverem prontos juntos. No Node.js, esse "pedir o combo todo de uma vez" se chama Promise.all().

// O que eu fazia antes (o jeito lento, esperando um por um)
console.time("Fluxo Sequencial");
const usuario = await getUsuarioLogado(); // demora 3s
const configuracoes = await getConfiguracoes(); // demora 2s
console.timeEnd("Fluxo Sequencial"); // Total de espera: 5 segundos

// O que faz sentido quando um dado não depende do outro
console.time("Fluxo Paralelo");
const [usuario, configuracoes] = await Promise.all([
  getUsuarioLogado(),
  getConfiguracoes()
]);
console.timeEnd("Fluxo Paralelo"); // Total de espera: 3 segundos (o tempo do mais lento)

Bug, insight ou decisão inesperada

A ficha caiu mesmo quando eu botei esse simples console.time() no meu código. O insight principal não foi apenas sobre a linguagem em si, mas sobre o fluxo de dados e de tempo do meu próprio sistema. Eu estava enfileirando coisas no JavaScript por pura ingenuidade, matando completamente a maior vantagem do Node.js: ser não-bloqueante e eficiente na gestão de chamadas de I/O.

Foi uma decisão inesperada ter que parar tudo o que eu estava fazendo na feature de negócio só para reescrever a base de chamadas assíncronas do projeto. Depois de entender o Promise.all(), passei a tarde inteira caçando outros lugares no código onde eu estava fazendo "pedidos separados no fast-food". Acabei encontrando três ou quatro endpoints cruciais que estavam sofrendo do mesmo problema de gargalo artificial causado pelo meu próprio desconhecimento da ferramenta.

O que fica

Aprender a sintaxe das linguagens e ferramentas é, de longe, a parte mais fácil de ser um desenvolvedor. Você lê a documentação em dez minutos, copia um trecho e já consegue escrever. Entender o modelo mental por trás dela, sacar o que a engine do V8 está fazendo por baixo dos panos com a call stack, a fila de microtasks e o event loop, é onde o jogo realmente acontece.

Não dá pra simplesmente jogar await em tudo e esperar que a linguagem resolva sozinha a arquitetura do seu sistema. Escrever código síncrono disfarçado de assíncrono é um erro silencioso: não quebra a build, não estoura erro no console, mas destrói a experiência de quem usa a sua aplicação. Agora, antes de colocar um await no começo de uma linha, eu sempre paro e me pergunto: "A próxima linha realmente precisa esperar isso aqui terminar antes de começar?". Se a resposta for não, eu já sei muito bem o que fazer.

É isso.

#javascript#nodejs#aprendizado