Índice
- O problema que Promises vieram resolver
- Anatomia de uma Promise
- Encadeamento com .then()
- Tratamento de erros: .catch() e .finally()
- Promise.all, Promise.race e os outros métodos estáticos
- Async/Await é só açúcar sintático — mas muda tudo
- Armadilhas de produção: erros que já vi em PRs reais
- FAQ
- Checklist antes do próximo deploy
- Próximos passos
O problema que Promises vieram resolver
Callback hell não é só feio — é um problema real de raciocínio. Você perde o fio de execução, o tratamento de erro se fragmenta em vários lugares, e qualquer refatoração vira pesadelo. Se você já tentou debugar algo assim num sistema em produção, sabe exatamente do que estou falando:
getUser(id, function(err, user) {
if (err) return handleError(err);
getOrders(user.id, function(err, orders) {
if (err) return handleError(err);
getInvoice(orders[0].id, function(err, invoice) {
if (err) return handleError(err);
});
});
});
Cada nível de indentação é um nível a mais de contexto mental que você precisa manter. Cada if (err) repetido é uma armadilha esperando o momento em que alguém esquece de colocar o return. O custo real não está no estilo — está nas horas de debug e nas regressões silenciosas.
Promises chegaram no ES6 para linearizar esse fluxo. Não para eliminar a assincronia — para torná-la gerenciável. A ideia central é simples: uma Promise representa um valor que ainda não existe, mas que vai existir (ou falhar) no futuro.
Anatomia de uma Promise
Uma Promise é um objeto que encapsula uma operação assíncrona e expõe seu resultado quando ela termina — com sucesso ou com falha.
const promise = new Promise((resolve, reject) => {
const sucesso = true;
if (sucesso) {
resolve("valor resultante");
} else {
reject(new Error("algo deu errado"));
}
});
Os três estados possíveis
Uma Promise tem exatamente três estados: pending (ainda em execução), fulfilled (resolvida com sucesso) e rejected (falhou). Uma vez que sai de pending, o estado não muda mais — isso é garantia da spec ECMAScript e significa que você nunca vai receber dois resultados diferentes da mesma Promise.
pending ──→ fulfilled (resolve foi chamado)
╰──→ rejected (reject foi chamado ou exceção lançada)
Na prática, você raramente cria Promises do zero. A maioria das APIs modernas já retorna Promises: fetch, fs.promises, drivers de banco de dados, clientes HTTP. O que você precisa dominar é como consumi-las.
Atenção ao executor síncrono: a função passada para
new Promise()roda de forma síncrona. O que é assíncrono é a resolução. Isso pega muita gente desprevenida:
console.log("antes");
new Promise((resolve) => {
console.log("dentro do executor");
resolve("ok");
});
console.log("depois");
// Saída:
// antes
// dentro do executor
// depois
Encadeamento com .then()
Aqui está o poder real das Promises. Cada .then() retorna uma nova Promise, o que permite encadear operações sequenciais de forma legível.
Imagine um fluxo de e-commerce: buscar o usuário, depois buscar os pedidos dele, depois buscar a fatura do pedido mais recente.
fetch("/api/users/42")
.then(response => response.json())
.then(user => fetch(`/api/orders?userId=${user.id}`))
.then(response => response.json())
.then(orders => fetch(`/api/invoices/${orders[0].id}`))
.then(response => response.json())
.then(invoice => {
renderInvoice(invoice);
});
O que está acontecendo: cada .then() recebe o valor resolvido da Promise anterior. Se o callback retorna um valor simples, ele é passado para o próximo .then(). Se retorna uma Promise, o chain espera ela resolver antes de continuar.
Promise.resolve(1)
.then(n => n + 1)
.then(n => n * 3)
.then(console.log);
// → 6
Promise.resolve(1)
.then(n => Promise.resolve(n + 1))
.then(n => n * 3)
.then(console.log);
// → 6 (o chain desempacota a Promise automaticamente)
Nos dois casos o resultado é o mesmo. Isso é importante: o chain sempre "achata" uma Promise retornada, nunca entrega uma Promise dentro de outra.
Tratamento de erros: .catch() e .finally()
.catch(fn) é equivalente a .then(null, fn) — captura qualquer rejeição no chain anterior, independente de onde ela aconteceu.
fetch("/api/dados")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(dados => processar(dados))
.catch(err => {
console.error("Falhou em algum ponto do chain:", err.message);
})
.finally(() => {
esconderLoading();
});
.finally() roda sempre — independente de sucesso ou falha — e não recebe o valor resolvido nem o erro. É o lugar certo para limpar estado: esconder loaders, fechar conexões, liberar locks.
O comportamento que pega muita gente
Um .catch() que não relança o erro transforma a Promise de volta para fulfilled:
Promise.reject(new Error("ops"))
.catch(err => {
console.log("capturou:", err.message);
return "recuperado";
})
.then(val => console.log(val));
// → capturou: ops
// → recuperado
Se você colocar um .then() depois do .catch(), a execução continua normalmente com o valor retornado pelo catch. Isso é útil para fallbacks, mas pode gerar comportamentos inesperados se você não estiver prestando atenção na estrutura do chain.
Promise.all, Promise.race e os outros métodos estáticos
Quando você precisa coordenar múltiplas Promises ao mesmo tempo, os métodos estáticos são o caminho.
Promise.all — paralelo, falha rápido
Aguarda todas resolverem. Se qualquer uma rejeitar, rejeita imediatamente com o erro dessa Promise.
const [usuario, pedidos, config] = await Promise.all([
buscarUsuario(id),
buscarPedidos(id),
buscarConfig(),
]);
Se cada operação leva 300ms, Promise.all termina em ~300ms. Em sequência com três await separados, seriam ~900ms. Parece óbvio, mas é comum ver código encadeando await desnecessariamente quando as operações não dependem uma da outra.
Promise.allSettled — paralelo, sem falha rápida
Como Promise.all, mas nunca rejeita. Retorna o resultado de cada Promise com seu status.
const resultados = await Promise.allSettled([
buscarDados("fonte-a"),
buscarDados("fonte-b"),
buscarDados("fonte-c"),
]);
resultados.forEach(resultado => {
if (resultado.status === "fulfilled") {
processar(resultado.value);
} else {
logErro(resultado.reason);
}
});
Use quando falhas parciais são toleráveis — por exemplo, buscar dados de múltiplas fontes e processar o que vier.
Promise.race e Promise.any
Promise.race — resolve ou rejeita com a primeira Promise que finalizar, independente do resultado.
Promise.any — resolve com a primeira que tiver sucesso. Só rejeita se todas falharem.
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout após 5s")), 5000)
);
const resultado = await Promise.race([fetchDados(), timeout]);
Tabela de comparação
| Método | Execução | Quando rejeita | Caso de uso |
|---|---|---|---|
Promise.all | Paralela | Qualquer falha | Operações obrigatórias, todas necessárias |
Promise.allSettled | Paralela | Nunca | Batch com falhas toleráveis |
Promise.race | Paralela | Primeira rejeição ou resolução | Timeout, corrida entre fontes |
Promise.any | Paralela | Todas falharem | Fallback automático entre fontes |
await sequencial | Sequencial | Primeira falha | Operações dependentes em série |
Async/Await é só açúcar sintático — mas muda tudo
async/await não substitui Promises — compila para elas. O que muda é a legibilidade e o controle de escopo.
function buscarDadosUsuario(id) {
return getUser(id)
.then(user => getProfile(user.profileId))
.then(profile => ({ ...user, profile }));
}
Esse código tem um bug silencioso: user não existe no escopo do segundo .then(). Cada callback tem seu próprio escopo isolado, então você precisa passar dados adiante explicitamente ou aninhar .then() — o que nos leva de volta ao callback hell.
async function buscarDadosUsuario(id) {
const user = await getUser(id);
const profile = await getProfile(user.profileId);
return { ...user, profile };
}
Com async/await, user está disponível em toda a função. O fluxo é linear, o escopo é limpo, e o stack trace quando algo falha aponta para a linha exata.
Esse bug específico apareceu num PR de uma empresa que trabalhei. O comentário do revisor foi curto: "Vai funcionar? Talvez. Vai ser mantido daqui a seis meses? Não."
Armadilhas de produção: erros que já vi em PRs reais
1. Esquecer de retornar a Promise dentro do .then()
.then(user => {
getProfile(user.id);
})
.then(profile => {
console.log(profile);
})
getProfile() é chamado, mas o chain não espera ele resolver. O próximo .then() recebe undefined. O erro não aparece no console — o código silenciosamente faz a coisa errada.
.then(user => {
return getProfile(user.id);
})
.then(profile => {
console.log(profile);
})
2. Await dentro de .forEach()
ids.forEach(async id => {
await processarItem(id);
});
console.log("feito");
forEach não tem como esperar callbacks assíncronos. Ele dispara todos os processarItem e segue em frente. O console.log("feito") roda antes de qualquer item terminar de processar. Sem erro, sem aviso — comportamento não intencional silencioso.
for (const id of ids) {
await processarItem(id);
}
await Promise.all(ids.map(id => processarItem(id)));
Use for...of quando precisar de sequência. Use Promise.all com .map() para paralelo com controle.
3. Não tratar rejeições
fetch("/api/dados").then(r => r.json()).then(processar);
No Node.js 15+ e em navegadores modernos, uma Promise rejeitada sem handler causa UnhandledPromiseRejection — que pode derrubar o processo ou ser logado como erro crítico.
fetch("/api/dados")
.then(r => r.json())
.then(processar)
.catch(err => console.error(err));
try {
const dados = await fetch("/api/dados").then(r => r.json());
processar(dados);
} catch (err) {
console.error(err);
}
4. Promise dentro de Promise sem await
async function salvar(dados) {
db.save(dados).then(() => {
console.log("salvo");
});
}
A função salvar retorna uma Promise que resolve imediatamente — antes do db.save() terminar. Quem fizer await salvar(dados) vai pensar que o dado foi salvo quando na verdade a operação ainda está em andamento.
async function salvar(dados) {
await db.save(dados);
console.log("salvo");
}
5. Misturar .then() e async/await no mesmo módulo sem consistência
Não é errado misturar, mas criar um padrão inconsistente dentro do mesmo arquivo dificulta code review e aumenta a chance de bugs de escopo. Escolha um estilo por módulo e seja consistente.
6. Usar Promise.all quando as operações dependem uma da outra
const [user, orders] = await Promise.all([
buscarUsuario(id),
buscarPedidos(id),
]);
Isso funciona porque buscarPedidos recebe o id diretamente. Mas se você precisasse do user.accountId para buscar os pedidos, Promise.all não funcionaria — as duas Promises são criadas ao mesmo tempo, antes de qualquer uma resolver.
FAQ
O que é uma Promise em JavaScript?
Uma Promise é um objeto que representa o resultado eventual de uma operação assíncrona. Ela pode estar em três estados: pending (aguardando), fulfilled (concluída com sucesso) ou rejected (falhou). Uma vez que sai de pending, o estado é imutável.
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve("pronto"), 1000);
});
p.then(val => console.log(val));
Uma Promise cancelada para de executar?
Não. Promises em JavaScript não são canceláveis nativamente. Uma vez criada, a operação roda até o fim — mesmo que você ignore o resultado. Para cancelamento real, use AbortController com fetch:
const controller = new AbortController();
fetch("/api/dados", { signal: controller.signal })
.then(r => r.json())
.catch(err => {
if (err.name === "AbortError") console.log("cancelado");
});
controller.abort();
Qual a diferença entre async function e retornar Promise.resolve()?
O comportamento final é equivalente — ambas retornam uma Promise. A diferença prática é que async function garante que qualquer exceção lançada internamente seja convertida em rejeição, enquanto uma função comum que lança antes de retornar a Promise vai lançar de forma síncrona e não vai ser capturada por .catch().
function comErroSincrono() {
throw new Error("ops");
return Promise.resolve(42);
}
async function comErroAsync() {
throw new Error("ops");
}
comErroSincrono();
comErroAsync().catch(err => console.error(err));
Promise.all falha rápido. Como processar os que deram certo?
Use Promise.allSettled. Ele nunca rejeita — retorna um array com o status de cada Promise:
const resultados = await Promise.allSettled([op1(), op2(), op3()]);
const sucessos = resultados
.filter(r => r.status === "fulfilled")
.map(r => r.value);
Por que await fora de async dá erro de sintaxe?
Até o ES2022, await só era válido dentro de funções async. A partir do ES2022, top-level await é suportado em módulos ES (arquivos .mjs ou "type": "module" no package.json). Em scripts comuns, ainda precisa de uma função async envolvendo.
Posso usar .then() em funções async?
Sim. Uma função async retorna uma Promise, então você pode encadear .then() normalmente. Misturar os dois estilos é legítimo, desde que seja intencional e consistente dentro do módulo.
Checklist antes do próximo deploy
Antes de fazer merge de qualquer código com operações assíncronas, passe por estes pontos:
- Todo
.then()que chama uma função async temreturn? - Algum
forEachestá recebendo callbackasync? Substitua porfor...ofouPromise.allcom.map() - Toda Promise tem um
.catch()ou está dentro de umtry/catch? - Operações independentes estão usando
Promise.allem vez deawaitsequencial? - Alguma função
asynctem Promise interna semawait? - O tratamento de erro está no nível certo — não apenas logando, mas respondendo adequadamente?
Próximos passos
- Estude
AbortControllerse trabalha comfetche precisa de cancelamento real - Conheça
p-limitno npm para controle de concorrência quando o volume de operações async crescer - Revise código legado que usa
forEachcomasync— o comportamento provavelmente não é o esperado