DEV Community

Henrique Reis
Henrique Reis

Posted on

Promises: estados, filas, encadeamento e relação com Event Loop

Uma Promise é um objeto que representa o resultado eventual de uma operação assíncrona, como uma chamada de API, permitindo lidar com resultados e erros de forma mais organizada do que os antigos callbacks. Usando métodos como .then() para sucesso e .catch() para falhas, ele funciona como um "contrato" de que um valor estará disponível agora, daqui a pouco ou nunca.


Estados de uma Promise

Uma Promise pode estar em apenas um dos três estados abaixo:

  • pending - ainda está em execução
  • fulfilled - foi concluída com sucesso
  • rejected - foi concluída com erro

Esses estados são imutáveis: uma vez fulfilled ou rejected, a Promise não voltará para pending


Por que Promises existem?

Para resolver o problema do callback hell e trazer um modelo mais previsível, encadeável e configável para operações assíncronas.

Com Promises conseguimos:

  • Evitamos pirâmides de callback
  • Tratamos erros de forma unificada
  • Encadeamos operações de forma mais clara
  • Temos integrações perfeitas com async/await

Criando uma Promise:

const promise = new Promise((resolve, reject) => {
    const sucesso = true

    if (sucesso) {
        resolve("Working!")
    } else {
        reject("Something wrong!")
    }
})
Enter fullscreen mode Exit fullscreen mode

A função passada para o construtor recebe dois parâmetros:

  • resolve(value) => finaliza com sucesso.
  • reject(value) => finaliza com erro (o reject não espera um erro mas é uma boa prática rejeitar com um objeto Error para facilitar o debugging, stack trace e identificar falhas -- reject(new Error('error))).

Consumindo Promises:

getUser()
    .then(user => getOrder(user.id))
    .then(orders => sendEmail(orders))
    .catch(error => console.log(error)) 
Enter fullscreen mode Exit fullscreen mode

Cada .then() adiciona uma nova microtask no Event Loop (explicado mais afundo durante o estudo)

Outro exemplo de encadeamento:

Promise.resolve(2)
    .then(num => num * 2)
    .then(num => num + 3)
    .then(num => console.log(num)) // 7
Enter fullscreen mode Exit fullscreen mode

Uso do .finally()

Executa sempre, independemente de sucesso ou erro

fetch("/api")
  .then(res => console.log("sucesso"))
  .catch(err => console.error("erro"))
  .finally(() => console.log("sempre executa"));
Enter fullscreen mode Exit fullscreen mode

Relação das Promises com o Event Loop

Como já sabemos (se chegou até aqui e ainda não tiver noção básica de como o Event Loop funciona, recomendo fortemente que volte e entenda um pouco sobre antes de continuar), o Event Loop é o coração da execução assíncrona no JavaScript.

Ele controla a ordem em que o código é executado e coordena todas as operações assíncronas -- inclusive Promises, é essencial entender como elas interagem entre sí.

O JavaScript é single-threaded, o ambiente não

O motor JavaScript roda em uma única thread, porém o ambiente (Node.js ou navegador) possui várias APIs assíncronas (Web APIs e libuv) que operam fora dessa thread e devolvem o resultado via filas gerenciadas pelo Event Loop.

Promises utilizam a microtask queue, que tem prioridade sobre callbacks de setTimeout por exemplo, que ficam na macrotask queue

O ciclo do Event Loop segue esta ordem:

  1. Executa todo o código síncrono
  2. Executa todas as microtasks pendentes
  3. Executa uma macrotask (se não tiver mais microtasks pendentes)
  4. Repete o ciclo

Por isso que Promises são mais rápidas que timeouts:

setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('microtask'))

// microtask
// timeout
Enter fullscreen mode Exit fullscreen mode

Mas quando uma Promise é resolvida, elas são tratadas como callbacks?

Conceitualmente, sim mas uma diferença fundamental

Quando uma Promise é resolvida ou rejeitada, o callback associado a ela não executa imediatamente, ele é agendado na microtask queue, como dito anteriormente.

Promise.resolve(10).then(x => console.log(x));
Enter fullscreen mode Exit fullscreen mode

A execução inicial:

  • a chamada Promise.resolve(10) é executada na Call Stack
  • a Promise é criada e já nasce no estado fulfilled
  • a chamada .then() também é executada na Call Stack

O .then():

  • não executa o callback
  • apenas registra um Promise Reaction Record
  • esse reaction é armazenado internamente pelo motor JS
  • nada é enfileirado ainda

Quando a Promise é fulfilled ou rejected:

  • o motor JS percorre os Promise Reaction Records registrados
  • para cada reaction, cria um Promise Reaction Job
  • esse job contém o callback registrado no then/catch/finally
  • o motor JS agenda internamente esse job como uma microtask
  • a microtask é colocada na microtask queue

Ou seja, o que realmente acontece é que o motor JavaScript percorre a lista de Promise Reaction Records e, para cada um deles, cria um PromiseReactionJob. Esse job é então agendado na microtask queue. É dentro desse reaction job que o callback registrado no then, catch ou finally é efetivamente executado.

Modelo mental do que acontece, para ter uma visualização do fluxo:

.then()
  ↓
cria um Promise Reaction Record (armazena o callback)
  ↓
quando a Promise resolve/reject
  ↓
o motor JS cria um PromiseReactionJob para cada Promise Reaction Record registrado
  ↓
job onde o callback está registrado (then/catch/finally)
  ↓
agenda o job na Microtask Queue
  ↓
executa na Call Stack
Enter fullscreen mode Exit fullscreen mode

O ponto que muda tudo

No caso das Promises, todo o fluxo acontece inteiramente dentro do motor JavaScript. Não há envolvimento de Web APIs, libuv ou eventos externos. O agendamento ocorre por meio do job scheduling interno do próprio engine.

Por isso, no funcionamento do Event Loop, os callbacks enfileirados como microtasks sempre têm prioridade sobre os enfileirados como macrotasks.

Como as Promises são resolvidas internamente pelo motor JavaScript, seus reaction jobs são agendados como microtasks e, por esse motivo, são sempre executados antes de qualquer macrotask.


Conclusão

Ao longo deste estudo, vimos que Promises vão muito além de uma simples alternativa a callbacks. Elas introduzem um modelo interno de execução assíncrona que é tratado diretamente pelo motor JavaScript, sem depender de ambiente externo.

Entender que:
.then(), .catch() e .finally() não executam callbacks imediatamente, mas registram Promise Reaction Records, que posteriormente geram PromiseReactionJobs nos quais são agendados como microtasks, é o ponto que realmente muda a forma como enxergamos o Event Loop.

Justamente esse mecanismo interno que explica por que Promises são previsíveis, se encadeiam de forma segura e sempre têm prioridade sobre macrotasks, como setTimeout, mesmo com delay zero.

Quando conseguimos entender esse fluxo, muitos "comportamentos estranhos" que nos deparamos durante o dia a dia no desenvolvendo em JavaScript ou TypeScript, deixam de ser mágicos e passam a fazer mais sentido. Promises não são mais uma caixa-preta -- elas são apenas jobs bem organizados, executados no momento certo

Se você quiser avançar ainda mais, o próximo passo natural seria entender como podemos realizar tratativas de erros, como os métodos especiais como Promise.all, Promise.allSettled e Promise.race funcionam e também como o async/await se apoia exatamente nesse mesmo mecanismo, apenas oferecendo uma sintaxe mais legível sobre as Promises que já conhecemos.

Top comments (0)