Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Promise em JavaScript: then, catch e finally

Os três estados de uma Promise, por que then sempre devolve outra Promise e o return esquecido que quebra a cadeia — com cada saída rodada no Node 24.

Rodolfo Mori5 min de leitura

Uma Promise é um objeto que representa um valor que ainda não existe. Ela é devolvida na hora, vazia, e é preenchida depois — com o resultado, se der certo, ou com o motivo, se der errado.

Você faz três coisas com ela: then para o caminho feliz, catch para o erro, finally para o que precisa acontecer nos dois casos. A parte que confunde é que cada um desses três devolve uma nova Promise, e é isso que permite encadear.

O protocolo de retirada de um pedido que ainda não ficou pronto

Quando você pede uma roupa na lavanderia, recebe um protocolo antes de receber a roupa. O papel não contém o resultado; ele representa a entrega futura e permite combinar o que fazer quando ficar pronta ou quando houver um problema. A roupa pode chegar depois, mas o protocolo existe agora.

Uma Promise é esse representante de um resultado futuro. Ela começa pending, passa uma vez para fulfilled ou rejected e cada then, catch ou finally devolve outro protocolo — outra Promise. No primeiro exemplo, anote a ordem em que os logs deveriam aparecer antes de rodar; a saída mostra que receber a Promise e receber o valor são momentos diferentes.

new Promise recebe uma função — o executor — com dois parâmetros: resolve e reject. O executor roda imediatamente; quem espera é quem chamou.

js
function consultarFrete(cep) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!/^\d{8}$/.test(cep)) {
        reject(new Error(`CEP inválido: ${cep}`));
        return;
      }
      resolve({ cep, prazo: 4, valor: 24.9 });
    }, 300);
  });
}

const promessa = consultarFrete('01310100');
console.log(promessa);

promessa.then((frete) => {
  console.log('chegou:', frete);
  console.log(promessa);
});
Promise { <pending> } chegou: { cep: '01310100', prazo: 4, valor: 24.9 } Promise { { cep: '01310100', prazo: 4, valor: 24.9 } }

O mesmo objeto, impresso duas vezes: primeiro &lt;pending&gt;, depois com o valor dentro. Foi só o estado que mudou — a Promise nunca foi substituída.

Com um CEP inválido, o reject leva o fluxo para o catch:

js
consultarFrete('0131')
  .then((frete) => console.log('frete:', frete.valor))
  .catch((e) => console.log('recusado:', e.message));
recusado: CEP inválido: 0131

Repare no return logo depois do reject, no primeiro exemplo. Ele não é decoração: sem ele, o executor continuaria rodando e chamaria o resolve também.

Três estados, e nenhuma volta

Uma Promise é pending até virar fulfilled (resolvida) ou rejected (rejeitada). Essa transição acontece uma vez só, e é irreversível. Tentar mudar de novo não dá erro, não dá aviso: simplesmente não acontece.

js
const pagamento = new Promise((resolve, reject) => {
  resolve('aprovado');
  reject(new Error('cartão recusado'));
  resolve('aprovado de novo');
  console.log('o executor continua rodando depois do resolve');
});

pagamento
  .then((status) => console.log('then:', status))
  .catch((e) => console.log('catch:', e.message));
o executor continua rodando depois do resolve then: aprovado

Duas leituras importantes na saída. O reject foi ignorado: a Promise já estava resolvida. E o console.log do executor rodou antes do then — o corpo do executor é síncrono; só o que você pendura com then é que vai para a fila de microtasks descrita em JavaScript assíncrono.

then devolve uma Promise nova

Cada then cria um elo. O valor devolvido dentro dele vira o valor recebido pelo próximo — e um then sem return entrega undefined adiante:

js
const buscarPedido = (id) => Promise.resolve({ id, cliente: 'Ana', itens: 3, total: 479.7 });

buscarPedido(8241)
  .then((pedido) => {
    console.log('1 —', pedido.cliente, 'R$', pedido.total);
    return pedido.total;
  })
  .then((total) => {
    console.log('2 — com frete:', total + 24.9);
    return total + 24.9;
  })
  .then((total) => {
    console.log('3 — em centavos:', Math.round(total * 100));
  })
  .then((valor) => {
    console.log('4 — o then anterior não devolveu nada:', valor);
  });
1 — Ana R$ 479.7 2 — com frete: 504.59999999999997 3 — em centavos: 50460 4 — o then anterior não devolveu nada: undefined

O 504.59999999999997 é ponto flutuante, não bug da cadeia — o mesmo assunto que aparece em variáveis em JavaScript quando se soma dinheiro. Por isso a linha seguinte converte para centavos.

O return que falta quebra tudo

Este é o erro número um de quem está aprendendo Promise. A cadeia parece certa, o encadeamento parece certo, e o segundo then recebe undefined:

js
const buscarCliente = (id) =>
  new Promise((resolve) => setTimeout(() => resolve({ id, nome: 'Ana', cep: '01310100' }), 200));

const consultarFrete = (cep) =>
  new Promise((resolve) => setTimeout(() => resolve({ cep, valor: 24.9 }), 200));

buscarCliente(31)
  .then((cliente) => {
    consultarFrete(cliente.cep); // faltou o return
  })
  .then((frete) => {
    console.log('frete:', frete);
    console.log('valor:', frete.valor);
  });
frete: undefined file:///private/tmp/loja-async/promise-sem-return.mjs:13 console.log('valor:', frete.valor); ^ TypeError: Cannot read properties of undefined (reading 'valor') at file:///private/tmp/loja-async/promise-sem-return.mjs:13:33 Node.js v24.16.0

A consulta de frete até aconteceu — mas ninguém devolveu a Promise dela, então a cadeia não esperou e seguiu com undefined. Com o return, a cadeia passa a aguardar:

js
buscarCliente(31)
  .then((cliente) => consultarFrete(cliente.cep))
  .then((frete) => {
    console.log('frete:', frete);
    console.log('valor:', frete.valor);
  });
frete: { cep: '01310100', valor: 24.9 } valor: 24.9

catch pega o que estourou em qualquer elo

Um throw dentro de um then rejeita a Promise daquele elo. O fluxo pula todos os then seguintes e cai no primeiro catch:

js
Promise.resolve({ id: 8241, total: 479.7 })
  .then((pedido) => {
    if (pedido.total > 100) throw new Error('pedido acima do limite sem aprovação');
    return pedido;
  })
  .then((pedido) => console.log('nunca chega aqui', pedido.id))
  .catch((e) => console.log('catch pegou:', e.message))
  .then(() => console.log('e a cadeia volta ao normal'));
catch pegou: pedido acima do limite sem aprovação e a cadeia volta ao normal

A última linha é a parte que surpreende: depois do catch, a cadeia volta a ser bem-sucedida. O catch é o try/catch do mundo assíncrono — tratou, o programa continua. Se você quer que a falha continue subindo, precisa lançar de novo dentro do próprio catch.

finally roda nos dois desfechos

Serve para limpeza: desligar o spinner, fechar a conexão, liberar o botão.

js
function cobrar(cartao, valor) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (cartao.saldo < valor) {
        reject(new Error('saldo insuficiente'));
        return;
      }
      resolve({ autorizacao: 'A7X91', valor });
    }, 200);
  });
}

let carregando = true;

cobrar({ saldo: 100 }, 479.7)
  .then((recibo) => console.log('pago:', recibo.autorizacao))
  .catch((e) => console.log('falhou:', e.message))
  .finally(() => {
    carregando = false;
    console.log('carregando =', carregando);
  })
  .then(() => console.log('a cadeia continua depois do catch'));
falhou: saldo insuficiente carregando = false a cadeia continua depois do catch

finally não recebe argumento e não altera o valor que passa por ele. É de propósito: ele não é um elo de transformação, é um elo de limpeza.

Rejeição sem catch derruba o processo

Uma Promise rejeitada que ninguém trata não fica quieta. No Node ela encerra o programa; no navegador, aparece como Uncaught (in promise) no console.

js
function cobrar(valor) {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('cartão recusado pela operadora')), 200);
  });
}

cobrar(479.7).then((recibo) => console.log('pago:', recibo));

console.log('pedido registrado');
pedido registrado file:///private/tmp/loja-async/promise-sem-catch.mjs:3 setTimeout(() => reject(new Error('cartão recusado pela operadora')), 200); ^ Error: cartão recusado pela operadora at Timeout._onTimeout (file:///private/tmp/loja-async/promise-sem-catch.mjs:3:29) at listOnTimeout (node:internal/timers:605:17) at process.processTimers (node:internal/timers:541:7) Node.js v24.16.0

Repare que pedido registrado foi impresso e o processo morreu 200ms depois, longe da linha que “causou” o problema. Por isso a regra: toda cadeia termina em catch — ou é devolvida para alguém que vai encadear um.

Quando ainda vale escrever new Promise

Quase nunca. Se a função que você chama já devolve Promise — fetch, as APIs de node:fs/promises, qualquer driver moderno de banco — envolver aquilo em new Promise só adiciona um lugar a mais para errar. Este é o antipadrão mais comum:

js
// não faça isso
function buscarProdutos() {
  return new Promise((resolve, reject) => {
    fetch('http://localhost:4599/produtos')
      .then((r) => r.json())
      .then(resolve)
      .catch(reject);
  });
}

// faça isso
function buscarProdutosOk() {
  return fetch('http://localhost:4599/produtos').then((r) => r.json());
}

new Promise se justifica em dois casos: embrulhar uma API antiga de callback (fs.readFile no estilo (erro, dado)) e transformar tempo em valor, como um espera(ms) para retry com intervalo. Fora disso, componha as Promises que já existem.

A próxima lição pega exatamente estas cadeias e reescreve com async e await, que deixa o código com cara de síncrono sem mudar nada do que você acabou de ver. Depois vem Promise.all e companhia, para disparar várias de uma vez. O caminho completo está no guia de JavaScript.

  • promise
  • then
  • catch
  • finally
  • assincrono

Perguntas frequentes

Preciso aprender Promise se já existe async/await?
Precisa. O async/await é uma forma de escrever a mesma coisa: por baixo continua tudo Promise. Sem entender os três estados e o encadeamento, você não consegue ler a mensagem de erro nem usar Promise.all direito.
Qual a diferença entre then com dois argumentos e then seguido de catch?
O segundo argumento do then só pega a rejeição que veio de antes dele. O catch encadeado pega também o que estourar dentro do próprio then. Na prática, prefira catch — ele cobre os dois casos.
O que acontece se eu chamar resolve duas vezes?
Nada. A Promise muda de estado uma vez só; a segunda chamada é ignorada em silêncio, sem erro. O restante do executor continua rodando normalmente, o que costuma surpreender quem esquece o return depois do reject.
finally recebe o valor da Promise?
Não recebe argumento nenhum, e é de propósito: ele existe para limpeza que vale nos dois desfechos, como desligar o spinner. O valor original segue na cadeia para o próximo then sem ser alterado.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Usando Promises — developer.mozilla.org
  2. MDN — Promise — developer.mozilla.org

Continue por aqui