programandoai

Promises por dentro: then, catch e all

O async/await esconde uma engrenagem chamada Promise. Entenda como ela funciona, leia o código escrito com .then() e junte várias tarefas de uma vez.

Estudar esta lição interativa →

Na lição do async/await apareceu de relance a palavra Promise, "a promessa de um valor futuro". Chegou a hora de olhar essa engrenagem de perto, por três motivos práticos: o await funciona em cima de promises (saber o que há por baixo explica os erros estranhos); muito código que você vai ler na vida real, de tutoriais e bibliotecas, usa .then() em vez de await; e para disparar várias tarefas ao mesmo tempo existe uma ferramenta própria, o Promise.all.

Uma Promise é como o código de rastreio de uma encomenda. Quando você compra, a encomenda ainda não está na sua mão, mas você ganha na hora um objeto que a representa. Com ele você combina o que fazer "quando chegar" e "se der problema". E, uma vez que o pacote chegou (ou foi extraviado), o resultado não muda mais.

Pendente (pending)

O estado inicial: a tarefa ainda está em andamento, não há valor nem erro.

Cumprida (fulfilled)

Deu certo: a promessa tem um valor, que é entregue ao .then.

Rejeitada (rejected)

Deu errado: a promessa tem um motivo (normalmente um Error), que é entregue ao .catch. Uma promessa muda de estado uma única vez.

Criar uma promessa é escrever new Promise((resolve, reject) => { ... }): dentro da função você faz o trabalho e chama resolve(valor) quando der certo, ou reject(erro) quando falhar. No exemplo, a função esperar devolve uma promessa que se cumpre depois de um tempo. Quem consome encadeia .then: cada um recebe o valor da etapa anterior e o que ele devolve com return vira o valor da etapa seguinte. Repare na ordem das mensagens: o código seguiu em frente sem esperar.

function esperar(ms, valor) {
  return new Promise((resolve) => setTimeout(() => resolve(valor), ms));
}

console.log("pedido feito");
esperar(100, "pedido pronto")
  .then((texto) => {
    console.log("then 1:", texto);
    return texto.toUpperCase(); // o que o then devolve vira o valor do próximo then
  })
  .then((maiusculo) => console.log("then 2:", maiusculo))
  .finally(() => console.log("finally: roda sempre"));
console.log("o código seguiu em frente");

Rode e acompanhe: as duas primeiras linhas saem na hora, o resto só quando a promessa se cumpre.

E quando algo dá errado? O .catch captura a rejeição de qualquer etapa anterior da corrente (o papel do catch do try/catch), e o .finally roda sempre, com sucesso ou erro. Repare que o que o catch devolve também segue adiante: depois dele a corrente "volta ao normal".

const falha = new Promise((resolve, reject) => {
  reject(new Error("API fora do ar")); // a promessa foi QUEBRADA
});

falha
  .then(() => console.log("não chega aqui"))
  .catch((erro) => {
    console.log("catch:", erro.message);
    return "plano B"; // depois do catch, a corrente volta ao normal
  })
  .then((valor) => console.log("depois do catch:", valor))
  .finally(() => console.log("finally"));

A promessa foi rejeitada: o then é pulado, o catch trata e o próximo then recebe o plano B.

async/await é a mesma coisa com outra cara

Tudo o que o await faz é esperar uma promise se cumprir (como o .then), e o try/catch em volta de um await faz o papel do .catch. Por isso aquele fetch da lição passada também pode ser lido assim: fetch(url).then((r) => r.json()).then((dados) => ...). Uma função async sempre devolve uma promise, e é por isso que dá para chamar .then no resultado dela.

A segunda ferramenta é para tarefas independentes. Buscar o perfil e a lista de pedidos de um usuário em sequência (um await atrás do outro) demora a soma dos dois tempos; dispará-los juntos demora só o do mais lento. Promise.all recebe uma lista de promessas e devolve uma promessa com a lista de resultados na ordem da lista, não na ordem em que cada uma terminou. O preço: se uma delas for rejeitada, o all falha na hora. Quando você precisa do relatório de todas, mesmo das que falharam, use Promise.allSettled, que nunca rejeita e entrega um objeto com o status de cada uma.

function esperar(ms, valor) {
  return new Promise((resolve) => setTimeout(() => resolve(valor), ms));
}

// all: espera TODAS e devolve os resultados na ordem da lista, não na ordem de chegada.
Promise.all([esperar(300, "A"), esperar(100, "B"), esperar(200, "C")])
  .then((resultados) => console.log("all:", resultados));

// all desiste na primeira falha...
Promise.all([esperar(50, "ok"), Promise.reject(new Error("falhou"))])
  .catch((erro) => console.log("all falhou:", erro.message));

// ...já allSettled espera todas e conta o que aconteceu com cada uma.
Promise.allSettled([esperar(50, "ok"), Promise.reject(new Error("falhou"))])
  .then((relatorio) => console.log("allSettled:", relatorio.map((r) => r.status)));

As três linhas aparecem pela ordem de conclusão, mas o resultado do all vem na ordem da lista: A, B, C.

Por fim, uma pergunta que confunde muita gente: em que ordem as coisas acontecem? Primeiro roda todo o código síncrono (de cima para baixo). Só então o JavaScript atende o que ficou na fila: as chamadas de .then (as microtarefas) são atendidas antes de qualquer callback de setTimeout, mesmo com tempo zero.

console.log("1: o código síncrono começa");

setTimeout(() => console.log("4: setTimeout (fila de tarefas)"), 0);

Promise.resolve().then(() => console.log("3: then (fila de microtarefas)"));

console.log("2: o código síncrono termina");

Previsão: 1, 2, 3, 4. Rode para conferir.

Três tropeços clássicos

Esquecer o return dentro de um .then: a etapa seguinte recebe undefined. Esquecer de tratar a rejeição: uma promessa rejeitada sem .catch (ou sem try/catch em volta do await) vira um erro "não tratado" no console. E achar que a promessa roda "depois": o trabalho dentro de new Promise(...) começa na hora em que ela é criada; o que fica para depois é só o .then.

// Sua vez! Use .then e .finally para consumir a promessa abaixo.
function esperar(ms, valor) {
  return new Promise((resolve) => setTimeout(() => resolve(valor), ms));
}

// 1) chame esperar(100, "olá") e, com .then, mostre o valor no console
// 2) encadeie um segundo .then que mostre o tamanho do texto (valor.length)
// 3) termine com .finally mostrando "fim"

Encadeie then, then e finally e clique em Rodar.

← Voltar ao índice do curso