Esses dois conceitos são fundamentais para entender como o JavaScript funciona, e mais ainda, como o Node.js lida com tarefas que demoram, como requisições HTTP, acesso a banco de dados ou leitura de arquivos.
No meu último post, que se tratava de como as promises realmente funcionam, citei que o próximo passo seria entender como o async/await se apoiam nesse mecanismos de assíncronicidade para oferecer uma sintaxe mais legível, e hoje vou estar explicando um pouco como isso funciona.
O que é uma operação síncrona?
Síncrono significa que as instruções são executadas umas após a outra, em ordem, bloqueando a execução de outras coisas até que cada etapa atual termine
Uma analogia simples seria uma fila de pessoas sendo atendidas por uma única pessoa no caixa do supermercado. A próxima pessoa só será atendida depois que a anterior terminar.
Exemplo:
console.log("Início");
const result = calcularPeso(); // trava o programa até terminar
console.log(result);
console.log("Fim");
Se calcularPeso() levar 5 segundos, nada mais acontece durante esse tempo, o programa fica "travado". Mas na verdade o Event Loop do Node.js é travado e assim não executa mais nenhuma tarefa mas esse não é o tópico do post.
O que é uma operação assíncrona?
Assíncrono significa que o programa não espera a operação terminar. Ele segue para a próxima instrução, e a operação assíncrona acontece em segundo plano, sendo executada quando possível.
Uma outra pequena simples analogia seria como pedir uma pizza, você faz o pedido (assíncrono) e continua fazendo outras coisas (programa segue) e quando a pizza chega, alguém te avisa (callback/promise).
Exemplo:
console.log("Início");
setTimeOut(() => {
console.log("Operação assíncrona finalizada")
}, 2000);
console.log("Fim");
Output:
Início
Fim
Operação assíncrona finalizada
Mas porque entender isso é importante no Node.js?
Node.js é single-threaded ou seja, existe apenas uma thread principal executando o código JavaScript (por padrão).
Se você usa funções síncronas, bloqueia tudo (nenhuma requisição é atendida até a atual terminar).
Com operações assíncronas, podemos escalar para milhares de coisas executando sem bloquear o Event Loop.
Recursos assíncronos comuns:
-
setTimeOut,setInterval -
fetch,axios,request -
fs.readFile,fs.writeFile -
db.query()(uma consulta no banco de dados) -
socket.on()(eventos utilizando web socket) Promise, async e await
Interação de operações async com o Event Loop
O Event loop gerencia essas operações assíncronas
- O código assíncrono é enviado para as Web APIs ou Libuv
- Ao finalizarem, voltam via Microtask queue (se forem Promises) ou Macrotask queue (se forem callbacks comuns)
- A Call Stack vai gerenciando a execução
Conceito do await em funções assíncronas
O await é uma palavra-chave do JavaScript usada dentro de funções assíncronas. Ela serve para esperar a resolução de uma Promise antes de continuar a execução do código, sem bloquear a thread principal do ambiente.
O Node.js apesar de ser single-threaded como dito antes, é concorrente graças à sua arquitetura baseada em eventos
Quando executamos uma operação de I/O, como uma consulta ao banco de dados:
const users = await db.findAll()
O Node.js:
- Dispara a operação de I/O
- Entrega a tarefa ao libuv, que gerencia um thread pool responsável por lidar com chamadas ao sistema (como consultas, arquivos, sockets e etc)
- Enquanto o libuv trabalha, o Event Loop continua livre, processando outras requisições, timer e callbacks
- Quando a operação termina, o libuv notifica o Event Loop, que agenda a resolução da Promise na microtask queue
- O JavaScript engine retoma a execução da função extamente após o await
Ou seja, quando a operação assíncrona é disparada com o await, o JavaScript entende que devemos "pausar" a execução daquela função para esperar o resultado do que precisamos para prosseguir mas não deixa de executar outras funções por conta disso.
O comportamento do await
async function getUsers() {
console.log('antes');
const users = await db.findAll(); // <- pausa lógica
console.log('depois');
}
O que acontece internamente:
- O Node.js executa tudo até chegar no
await - O
db.findAll()retorna uma Promise pendente - O runtime "pausa logicamente" a função
getUsers, guardando seu contexto (variáveis locais, escopo, posição atual) - A função retorna imediatamente uma Promise pendente ao chamador
- Enquanto isso, o Node.js continua executando outras tarefas que aparecem no Event Loop
- Quando o banco responde, o callback da Promise é enfileirado na Microtask queue
- O Event Loop retorna a execução da função logo após o await
Mas o que significa "pausar a função"?
Pausar não seria bloquear a CPU e nem travar a thread principal do Node.
Significa apenas que a execução daquela função em sí é suspensa de forma cooperativa até que a Promise retorne um valor para dar continuidade.
Enquanto isso:
- O contexto da função fica salvo
- O Event Loop continua processando outras tarefas
- A thread principal nunca fica ociosa esperando algo
Explicação envolvendo duas funções
Quando eu estava realmente aprendendo o papel do async/await em funções assíncronas também ficava me perguntando: é fácil visualizar o que acontece com uma função, mas quando várias funções ao mesmo tempo trabalham dessa maneira?
Imagina que existam duas funções em execução simultaneamente:
async function A() {
const users = await db.findAll();
console.log('A terminou');
}
function B() {
console.log('B executando');
}
O fluxo das duas funções seria basicamente esse:
-
Aé chamado => executa até oawaite dispara a query para o banco de dados. - A query é enviada da Call Stack para a Libuv, que cuida dessa Promise no background.
- O Event Loop roda
Benquanto o banco processa a consulta da funçãoA. - Quando a Promise de
Aresolve, ela é re-enfileirada. - A função
Aretoma e finaliza comAresolvida.
Apenas a função A "pausou", o Node.js nunca parou de executar outras coisas, e esse exemplo com apenas duas funções pode se extender a milhares de funções simultâneas
Papeis dos componentes explicados
await: Suspende a execução da função até a Promise resolverPromise: Representa o resultado futuro de uma operação assíncronaLibuv: Executa operações de I/O fora da thread do Event LoopEvent Loop: Coordena execuções de tasks e microtasksMicrotask queue: Fila que contém Promises resolvidas aguardando execução da Call StackThread principal: Executa JavaScript, nunca bloqueada por await
Conclusão para fixar
O await é, essencialmente, uma forma elegante de “esperar sem travar”. Ele permite que você escreva código assíncrono com aparência de código síncrono, pausando a execução apenas daquela função até que uma Promise seja resolvida — sem bloquear o restante da aplicação.
Na prática, isso significa mais legibilidade e menos complexidade, evitando encadeamentos confusos de .then() ao trabalhar com Promises, e passa a ter um fluxo mais linear e fácil de entender. Ao mesmo tempo, por baixo dos panos, o comportamento continua sendo assíncrono e eficiente.
Em resumo, o await não torna o código síncrono, ele só faz parecer que é, enquanto mantém a vantagem da execução assíncrona.
Top comments (0)