JavaScript por baixo dos panos · parte 3 de 3
Promises e async/await no JavaScript: microtasks, encadeamento e fluxo assíncrono
Entenda o que Promises realmente representam, como then/catch/finally entram na microtask queue e como async/await reorganiza o fluxo assíncrono sem bloquear o JavaScript.
Nos dois artigos anteriores, a base foi esta: primeiro, como o JavaScript executa código; depois, como o runtime organiza assincronismo com Call Stack, Web APIs, Task Queue, Microtask Queue e Event Loop.
Agora o próximo passo é olhar para Promise e async/await sem tratar nada como mágica. A ideia aqui é entender onde essas abstrações entram naquele mesmo desenho que já começamos a montar antes.
Se você quiser revisar a base antes, vale ler Event Loop no JavaScript.
Neste artigo, a ideia é seguir a mesma linha dos anteriores: construir modelo mental. O foco aqui não é decorar método de API, e sim entender o que Promise realmente representa, como os handlers entram na Microtask Queue e o que async/await faz de verdade com a execução da função.
Antes de tudo: Promise não cria assincronismo
Vale deixar isso claro logo no começo.
Promise não cria assincronismo. O assincronismo já existe por causa do ambiente e das APIs assíncronas que iniciam operações fora da Call Stack.
O que Promise faz é representar, do lado do JavaScript, o resultado futuro dessa operação.
Ou seja:
fetchinicia uma operação assíncrona no ambientesetTimeoutagenda um timer no ambientePromiserepresenta o resultado futuro disso no seu código
Então, quando falamos em Promise, estamos falando muito mais de modelagem do resultado assíncrono do que da origem do assincronismo em si.
O que uma Promise representa
Uma Promise é um objeto que representa um valor que ainda não está disponível no momento atual.
Ela pode estar em três estados:
pendingfulfilledrejected
Enquanto a operação ainda não terminou, ela está pending.
Quando termina com sucesso, ela fica fulfilled.
Quando falha, ela fica rejected.
O ponto central aqui é simples:
- uma
Promisenão é o valor final - ela é o objeto que representa esse valor antes de ele estar disponível
O que existe dentro de uma Promise
Quando a gente fala em Promise, é útil imaginar que existe um objeto com alguns dados internos importantes.
Sem entrar em todos os detalhes da especificação, os mais relevantes aqui são:
- o estado atual da
Promise - o resultado associado a esse estado
- a lista de reações de sucesso registradas com
.then(...) - a lista de reações de erro registradas com
.catch(...)
Em termos mais próximos da especificação, isso costuma aparecer em explicações como:
[[PromiseState]][[PromiseResult]][[PromiseFulfillReactions]][[PromiseRejectReactions]]
O ponto não é decorar esses nomes. O que interessa aqui é a ideia:
- a
Promiseguarda seu estado - guarda seu resultado
- e guarda o que precisa acontecer quando esse estado mudar
É justamente isso que torna o comportamento de .then(...), .catch(...) e await previsível.
O executor de new Promise(...) roda na hora
Esse detalhe costuma confundir bastante no começo.
Muita gente olha para new Promise(...) e assume que tudo ali já nasceu assíncrono. Mas o executor do construtor roda imediatamente.
console.log("A")
new Promise((resolve) => {
console.log("B")
resolve("C")
}).then((value) => {
console.log(value)
})
console.log("D")A saída é:
A
B
D
CO que acontece aqui?
console.log("A")executa.- O construtor da
Promiseé chamado. - O executor roda imediatamente, então
console.log("B")acontece agora. resolve("C")resolve aPromise.- O callback do
.then(...)não executa agora; ele entra naMicrotask Queue. console.log("D")ainda faz parte do fluxo síncrono atual.- Depois que esse trecho termina, a microtask roda e imprime
C.
Esse exemplo mostra duas coisas importantes ao mesmo tempo:
- o executor da
Promiseé síncrono - os handlers registrados com
.then(...),.catch(...)e.finally(...)são assíncronos
O que resolve e reject fazem de verdade
resolve e reject não executam callbacks diretamente.
Eles apenas mudam o estado da Promise e registram o resultado daquele assentamento.
Depois disso, os handlers que estavam esperando essa resolução ou rejeição podem ser agendados.
Então vale separar bem as coisas:
resolveerejectassentam aPromise.then(...),.catch(...)e.finally(...)reagem a esse assentamento- essa reação não acontece imediatamente na mesma linha; ela entra no fluxo assíncrono via microtask
Aqui também vale um refinamento importante: quando falamos em "assentar" uma Promise, estamos dizendo que ela deixa de estar pending e passa a ficar resolvida ou rejeitada de forma definitiva.
Depois que isso acontece, ela não volta para pending nem troca de estado novamente.
.then(...) não continua a mesma Promise; ele cria outra
Esse é um dos pontos que mais vale fixar.
Quando você chama .then(...), não está "editando" a Promise original. Você está criando uma nova Promise ligada ao resultado do callback.
Promise.resolve(2)
.then((value) => {
return value * 2
})
.then((value) => {
console.log(value)
})Saída:
4O fluxo é este:
- a primeira
Promiseresolve com2 - o primeiro
.then(...)recebe2 - ele retorna
4 - esse
4resolve a próximaPromiseda cadeia - o segundo
.then(...)recebe4
Ou seja: cada passo da cadeia entrega um resultado para o próximo passo.
Esse é um bom momento para conectar com o artigo anterior: enquanto a Promise original ainda está pendente, o callback do .then(...) fica registrado como reação pendente. Quando a Promise assenta, essa reação é colocada na Microtask Queue, e o valor retornado por esse callback define o estado da próxima Promise da cadeia.
Quando um .then(...) retorna outra Promise
Aqui aparece uma das partes mais importantes do modelo mental.
Se um .then(...) retorna uma Promise, a próxima etapa da cadeia não recebe "uma Promise dentro de outra". Ela espera essa Promise terminar e adota o valor final dela.
Promise.resolve(2)
.then((value) => {
return Promise.resolve(value * 2)
})
.then((value) => {
console.log(value)
})Saída:
4Isso importa porque mostra que a cadeia de Promise já sabe lidar com Promises retornadas por callbacks.
É justamente por isso que encadeamento funciona tão bem: cada etapa pode devolver um valor simples ou uma Promise, e a próxima etapa recebe sempre o resultado já "desembrulhado".
Em outras palavras, a cadeia não vai acumulando camadas de Promise<Promise<...>> do jeito que muita gente imagina no começo. Ela adota o resultado final da Promise retornada e segue a partir dali.
Erros propagam pela cadeia
Outro ponto central é o caminho de erro.
Se um callback dentro de .then(...) lança erro, a próxima Promise da cadeia fica rejeitada.
Promise.resolve("ok")
.then(() => {
throw new Error("algo deu errado")
})
.catch((error) => {
console.log(error.message)
})Saída:
algo deu erradoIsso significa que, em uma cadeia de Promise:
- retornar um valor resolve a próxima etapa
- lançar um erro rejeita a próxima etapa
E o .catch(...) entra justamente para interceptar esse caminho de erro.
.finally(...) é encerramento, não transformação
O finally tem outro papel.
Ele não existe para transformar o valor que passa pela cadeia. Ele existe para rodar código quando a Promise termina, seja em sucesso ou em erro.
Promise.resolve("sucesso")
.then((value) => {
console.log(value)
})
.finally(() => {
console.log("encerrado")
})Saída:
sucesso
encerradoO ponto importante aqui é este:
finallyroda em sucesso e em erro- ele não transforma o valor — o resultado original passa por ele sem alteração
- ele serve melhor para cleanup, fechamento e lógica de encerramento
Por que .then(...) sempre parece "vir depois"
Aqui entra diretamente a ponte com o artigo anterior.
console.log("Start")
Promise.resolve("Operation with .then")
.then((value) => {
console.log(value)
})
console.log("End")Saída:
Start
End
Operation with .thenIsso acontece porque o callback de .then(...) entra na Microtask Queue.
Então a ordem real é:
- o trecho síncrono atual roda até o fim
- depois as microtasks pendentes são processadas
- aí o callback do
.then(...)roda
É exatamente aqui que Promise se conecta ao modelo de Event Loop que vimos antes.
async sempre devolve uma Promise
Quando uma função é marcada como async, ela sempre retorna uma Promise, mesmo quando você retorna um valor comum.
async function getNumber() {
return 42
}
getNumber().then((value) => {
console.log(value)
})Saída:
42Na prática, isso significa que:
return 42dentro de umaasync functionvira umaPromiseresolvida com42throw new Error(...)dentro de umaasync functionvira uma rejeição
Então async já coloca a função inteira dentro desse modelo baseado em Promise.
Isso também significa que uma async function participa da mesma lógica de encadeamento e microtasks que qualquer outra Promise.
await não bloqueia o JavaScript inteiro
O await não para o programa inteiro. Ele pausa a continuação da função assíncrona atual até a Promise ser resolvida.
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
async function main() {
console.log("antes do await")
await delay(1000)
console.log("depois do await")
}
main()
console.log("fora da função")A saída é:
antes do await
fora da função
depois do awaitO que acontece aqui?
main()começa.console.log("antes do await")executa.await delay(1000)pausa a continuação demain.- o restante do código fora da função continua, então
console.log("fora da função")roda. - quando a
Promiseresolve, a continuação da função é agendada como microtask. console.log("depois do await")executa.
Então o await não bloqueia a thread principal. Ele apenas reorganiza a continuação daquela função.
Pensando por baixo dos panos, o await divide a função em duas partes:
- o trecho que roda até encontrar o
await - a continuação que só volta a executar quando a
Promisefor resolvida
Essa continuação entra no mesmo mecanismo de microtasks que vimos antes.
await é uma forma melhor de expressar encadeamento
Por baixo dos panos, async/await continua dependendo de Promise e microtasks.
O ganho principal está na forma como o fluxo fica escrito.
Estas duas versões expressam a mesma ideia:
delay(1000)
.then(() => {
return delay(1000)
})
.then(() => {
console.log("fim")
})async function run() {
await delay(1000)
await delay(1000)
console.log("fim")
}O que muda não é a existência do assincronismo. O que muda é a legibilidade do fluxo.
Por isso dá para pensar em await como uma forma mais legível de expressar "espere o resultado desta Promise e continue depois".
await em sequência é serial
Se você faz isto:
function delayValue(ms, value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), ms)
})
}
async function sequential() {
const value1 = await delayValue(1000, "A")
const value2 = await delayValue(1000, "B")
console.log(value1, value2)
}
sequential()Essa função leva algo perto de 2 segundos.
Isso acontece porque a segunda operação só começa depois que a primeira terminou.
Ou seja: aqui o fluxo é serial.
O ponto realmente importante: quando as Promises começam
Agora veja este caso:
function delayValue(ms, value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), ms)
})
}
async function parallelStart() {
const promise1 = delayValue(1000, "A")
const promise2 = delayValue(1000, "B")
const value1 = await promise1
const value2 = await promise2
console.log(value1, value2)
}
parallelStart()Agora o tempo total fica perto de 1 segundo.
Isso mostra um ponto muito importante:
- o que define concorrência não é só o
await - o que realmente importa é quando as Promises foram criadas
Se você cria as operações antes, elas já começam a andar. O await só decide quando você vai esperar por cada resultado.
Promise.all(...) como coordenação, não como "mágica de paralelismo"
Promise.all(...) não cria paralelismo do nada. Ele coordena um conjunto de Promises que já foram iniciadas.
Promise.all([
Promise.resolve("A"),
Promise.resolve("B"),
Promise.resolve("C")
]).then((values) => {
console.log(values)
})Saída:
[ 'A', 'B', 'C' ]O comportamento importante aqui é:
- ele espera todas resolverem
- se uma falhar, a
Promiseretornada porPromise.all(...)rejeita imediatamente
Então o modelo mental é: Promise.all(...) coordena um conjunto de operações quando todas precisam dar certo.
O detalhe importante aqui é que ele não inicia essas operações por conta própria. Ele recebe Promises que já foram criadas e coordena o momento em que o conjunto pode ser considerado concluído.
Promise.allSettled(...) quando você quer olhar o conjunto inteiro
Às vezes você não quer falhar rápido. Você quer olhar o resultado do conjunto inteiro.
Promise.allSettled([
Promise.resolve("A"),
Promise.reject("erro"),
Promise.resolve("C")
]).then((results) => {
console.log(results)
})Saída:
[
{ status: 'fulfilled', value: 'A' },
{ status: 'rejected', reason: 'erro' },
{ status: 'fulfilled', value: 'C' }
]Cada item do resultado carrega o status e, dependendo do estado, o value ou o reason.
Então o modelo mental muda:
Promise.all(...): todas precisam dar certoPromise.allSettled(...): quero inspecionar o resultado de todas, independente de sucesso ou falha
Promise.race(...) e Promise.any(...)
Esses dois métodos parecem parecidos, mas resolvem coisas diferentes.
Promise.race(...)
Resolve ou rejeita com a primeira Promise que terminar.
Promise.race([
new Promise((resolve) => setTimeout(() => resolve("rápida"), 500)),
new Promise((resolve) => setTimeout(() => resolve("lenta"), 1000))
]).then((value) => {
console.log(value)
})Saída:
rápidaAqui o ponto é: "o que terminar primeiro vence", seja sucesso ou erro.
Um detalhe importante: as outras Promises não são canceladas. Elas continuam rodando normalmente — apenas seus resultados são ignorados. JavaScript não tem um mecanismo nativo de cancelamento de Promises.
Promise.any(...)
Resolve com a primeira Promise bem-sucedida.
Promise.any([
Promise.reject("erro 1"),
Promise.resolve("sucesso"),
Promise.reject("erro 2")
]).then((value) => {
console.log(value)
})Saída:
sucessoAqui o ponto é: "quero a primeira que der certo". Erros são ignorados até que uma Promise resolva com sucesso.
Se todas rejeitarem, Promise.any(...) lança um AggregateError contendo todos os motivos de rejeição.
Assim como no Promise.race(...), as demais Promises continuam rodando — seus resultados simplesmente não são usados.
Um erro comum: forEach com await
forEach não espera await do jeito que muita gente imagina.
const items = [1, 2, 3]
items.forEach(async (item) => {
await delayValue(1000, item)
console.log(item)
})
console.log("fim")Saída:
fim
1
2
3O console.log("fim") aparece primeiro porque o forEach não foi feito para coordenar fluxo assíncrono. Ele dispara cada callback e segue em frente sem esperar o await interno. Cada iteração cria uma Promise independente que o forEach simplesmente ignora.
Quando você precisa controle real de sequência ou concorrência, as alternativas são:
for...of com await para sequência:
for (const item of items) {
await delayValue(1000, item)
console.log(item)
}Aqui cada item espera o anterior terminar antes de continuar. O tempo total é proporcional ao número de itens.
.map(...) com Promise.all(...) para concorrência coordenada:
await Promise.all(
items.map(async (item) => {
await delayValue(1000, item)
console.log(item)
})
)Aqui todos os itens começam ao mesmo tempo e o código aguarda todos terminarem antes de continuar.
O modelo mental que vale guardar
Se eu tivesse que resumir o papel de Promise e async/await, eu guardaria isto:
Promiserepresenta o resultado futuro de uma operação assíncrona- o executor de
new Promise(...)roda imediatamente .then(...),.catch(...)e.finally(...)registram handlers que entram naMicrotask Queue- cada etapa da cadeia devolve uma nova
Promise - retornar outra
Promisefaz a cadeia esperar por ela e adotar seu resultado asyncsempre retornaPromiseawaitpausa a função atual, não o programa inteiro- sequência e concorrência dependem muito mais de quando você cria as
Promisesdo que doawaitisoladamente
Fechando
Depois que esse modelo mental fica claro, Promise e async/await deixam de parecer "um conjunto de APIs para decorar" e passam a fazer parte do mesmo desenho que já vimos no Event Loop.
Esse é o ponto que mais importa nesta série: cada camada nova faz mais sentido quando você entende como ela se encaixa na anterior.
Com essa base, o próximo passo natural é sair um pouco da mecânica e entrar mais em padrões práticos: concorrência controlada, batching, limites paralelos e composição de fluxos assíncronos.