DEV Community

Henrique Reis
Henrique Reis

Posted on

Síncrono, assíncrono e Async/Await: conectando os pontos

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");

Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

Output:

Início
Fim
Operação assíncrona finalizada
Enter fullscreen mode Exit fullscreen mode

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

  1. O código assíncrono é enviado para as Web APIs ou Libuv
  2. Ao finalizarem, voltam via Microtask queue (se forem Promises) ou Macrotask queue (se forem callbacks comuns)
  3. 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:

  1. Dispara a operação de I/O
  2. Entrega a tarefa ao libuv, que gerencia um thread pool responsável por lidar com chamadas ao sistema (como consultas, arquivos, sockets e etc)
  3. Enquanto o libuv trabalha, o Event Loop continua livre, processando outras requisições, timer e callbacks
  4. Quando a operação termina, o libuv notifica o Event Loop, que agenda a resolução da Promise na microtask queue
  5. 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');
}
Enter fullscreen mode Exit fullscreen mode

O que acontece internamente:

  1. O Node.js executa tudo até chegar no await
  2. O db.findAll() retorna uma Promise pendente
  3. O runtime "pausa logicamente" a função getUsers, guardando seu contexto (variáveis locais, escopo, posição atual)
  4. A função retorna imediatamente uma Promise pendente ao chamador
  5. Enquanto isso, o Node.js continua executando outras tarefas que aparecem no Event Loop
  6. Quando o banco responde, o callback da Promise é enfileirado na Microtask queue
  7. 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');
}
Enter fullscreen mode Exit fullscreen mode

O fluxo das duas funções seria basicamente esse:

  1. A é chamado => executa até o await e dispara a query para o banco de dados.
  2. A query é enviada da Call Stack para a Libuv, que cuida dessa Promise no background.
  3. O Event Loop roda B enquanto o banco processa a consulta da função A.
  4. Quando a Promise de A resolve, ela é re-enfileirada.
  5. A função A retoma e finaliza com A resolvida.

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 resolver

  • Promise: Representa o resultado futuro de uma operação assíncrona

  • Libuv: Executa operações de I/O fora da thread do Event Loop

  • Event Loop: Coordena execuções de tasks e microtasks

  • Microtask queue: Fila que contém Promises resolvidas aguardando execução da Call Stack

  • Thread 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)