Uma Promise tem 3 estados: pending, fulfilled e rejected. Uma vez liquidada (settled), é imutável — não pode voltar a pending nem mudar de estado. .then(cb) não executa cb imediatamente: enfileira cb na microtask queue, mesmo que a promise já esteja resolvida. Encadeamento de .then devolve uma nova promise a cada chamada; erros propagam automaticamente até o primeiro .catch. Promise.resolve(x) é açúcar sintático para criar uma promise já fulfillada — equivalente a new Promise((res) => res(x)), mas mais legível. Em Node.js, promises rejeitadas sem handler disparam o evento unhandledRejection; no Node 15+ o padrão passou a terminar o processo.
Por que .then() chama o callback mesmo quando a Promise já está resolvida — de forma assíncrona?
Se uma Promise já tem valor, por que não chamar o callback imediatamente? E por que esquecer um return dentro de .then() silencia erros sem aviso? As respostas estão na máquina de estados e na semântica da microtask queue que governa o comportamento interno de Promises.
O que é
Uma Promise é um objeto que representa o resultado eventual de uma operação assíncrona. Ela age como um proxy: você não tem o valor ainda, mas tem um objeto ao qual pode encadear handlers que serão executados quando o valor chegar (ou quando a operação falhar).
Os três estados
stateDiagram-v2
[*] --> pending: new Promise(executor)
pending --> fulfilled: resolve(value)
pending --> rejected: reject(reason)
fulfilled --> [*]: imutável
rejected --> [*]: imutável
note right of pending: executor roda síncronamente\nno construtor
note right of fulfilled: .then(cb) → cb enfileirado\nna microtask queue
note right of rejected: .catch(cb) → cb enfileirado\nna microtask queue
Uma promise liquidada (settled) é toda promise que não está mais em pending — seja ela fulfilled ou rejected. A especificação garante que, após atingir qualquer um dos estados finais, a promise nunca muda. Isso torna promises seguras para múltiplos consumidores: você pode chamar .then() dezenas de vezes na mesma promise e cada handler recebe o mesmo valor.
O termo resolved (resolvida) tem um significado técnico mais sutil: uma promise está “resolved” quando sua trajetória foi fixada — seja com um valor direto, seja “seguindo” outra promise (thenable). Uma promise pode estar resolved mas ainda pending, se ela foi resolvida com outra promise que ainda não liquidou.
O executor e sua execução síncrona
const p = new Promise((resolve, reject) => { // Este bloco executa AGORA, de forma síncrona console.log('executor rodando'); resolve(42);});console.log('depois do new Promise');// Output:// executor rodando// depois do new Promise
O executor roda síncronamente dentro do construtor — é o único ponto síncrono da API de Promise. O que acontece depois de resolve() ou reject() é que os handlers registrados com .then() são enfileirados como microtasks — e microtasks só rodam após o código síncrono atual terminar.
O .then() e a microtask queue
A propriedade mais importante e mais mal-compreendida das Promises:
const p = new Promise((resolve) => resolve(42));p.then((v) => console.log('then:', v));console.log('síncrono');// Output:// síncrono// then: 42
Mesmo que p já esteja fulfillada no momento em que .then() é chamado, o callback não roda imediatamente. Ele é enfileirado na microtask queue e só executa depois que todo o código síncrono do stack frame atual terminar. Isso é uma garantia da spec — handlers de Promise são sempre assíncronos, sem exceção.
Isso significa que o modelo de execução de Promises é previsível: você nunca precisa se perguntar se um .then() vai rodar agora ou depois. A resposta é sempre “depois do síncrono atual”.
Encadeamento — cada .then() retorna uma nova promise
const p1 = Promise.resolve(1);const p2 = p1.then((v) => v + 1); // p2 é uma NOVA promiseconst p3 = p2.then((v) => v * 2); // p3 é outra promise novap3.then((v) => console.log(v)); // 4
Cada chamada a .then() cria e retorna uma promise nova. O estado dessa nova promise depende do que o handler retorna:
O handler…
A nova promise…
Retorna um valor primitivo v
É fulfillada com v
Retorna outra promise q
Segue o estado de q (torna-se q)
Lança uma exceção
É rejeitada com o erro
Não tem handler para esse lado
Propaga o mesmo estado da promise pai
Essa última linha é o mecanismo de propagação de erros: se uma promise rejeitada não tem handler de rejeição em um .then(), a rejeição passa adiante — como uma exceção não capturada que sobe a pilha.
Por que importa
Promises são a fundação de toda a programação assíncrona moderna em JavaScript. async/await é syntactic sugar sobre Promises — um await expr é essencialmente um .then() em expr, com a diferença que o resto da função async é o callback.
Sem entender que .then() enfileira microtasks (não executa imediatamente), você vai criar bugs sutis de ordem de execução — especialmente ao misturar código síncrono com código async em testes ou em inicialização de serviços.
Sem entender que cada .then() retorna uma promise nova, você vai esquecer return dentro de chains e criar promises soltas que propagam erros silenciosamente — um dos bugs mais difíceis de debugar em codebases JavaScript.
Como funciona
1. Estados e imutabilidade
const p = new Promise((resolve, reject) => { resolve(42); reject(new Error('ignorado')); // sem efeito — já resolvida resolve(99); // sem efeito — já resolvida});p.then((v) => console.log(v)); // 42
Após a primeira chamada a resolve(), qualquer chamada subsequente — seja resolve() ou reject() — é silenciosamente ignorada. Isso é parte do contrato da Promise.
Cada .then() recebe o resultado do anterior e pode transformá-lo. A chain é lazy: os callbacks só rodam quando cada promise predecessora liquida.
3. Propagação de erros
Promise.resolve('ok') .then(() => { throw new Error('algo deu errado'); }) .then(() => { console.log('este nunca roda'); }) .catch((e) => { console.log('capturado:', e.message); // capturado: algo deu errado }) .then(() => { console.log('continua após o catch'); // roda normalmente });
Erros pulam todos os .then() intermediários que não têm handler de rejeição, e são capturados pelo primeiro .catch(). Após o .catch() completar sem lançar, a chain retorna ao estado fulfilled — o .then() seguinte ao .catch() executa normalmente.
Para rejeitar a chain após um .catch(), basta relançar o erro:
4. Promise.resolve(x) vs new Promise((res) => res(x))
// Equivalentes na práticaconst p1 = Promise.resolve(42);const p2 = new Promise((resolve) => resolve(42));// Promise.resolve com thenable — segue o estadoconst p3 = Promise.resolve(fetch('/api/data')); // mesma coisa que fetch('/api/data')
Promise.resolve(x) é idiomático para normalizar um valor que pode ser síncrono ou uma promise. Se x já é uma Promise nativa, o runtime retorna x diretamente sem criar uma nova promise — otimização garantida pela spec.
// RUIM — "Promise constructor anti-pattern"async function getData(url) { return new Promise((resolve, reject) => { fetch(url) .then((res) => resolve(res)) .catch((err) => reject(err)); });}// BOM — fetch já retorna uma Promiseasync function getData(url) { return fetch(url);}
Envolver uma promise em outra promise com o construtor é redundante e perigoso: se o código dentro do executor lançar uma exceção síncrona antes de chamar reject(), você perde o erro — ele vira uma rejeição não tratada invisível. Use o construtor apenas quando estiver adaptando uma API baseada em callbacks.
6. Deferred pattern — válido, mas evite como padrão
O “deferred pattern” extrai resolve e reject para fora do executor, criando um objeto que terceiros podem resolver:
function createDeferred() { let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject };}// Usoconst deferred = createDeferred();// Em algum lugar do códigodeferred.resolve(42);// Em outro lugardeferred.promise.then((v) => console.log(v));
Esse padrão tem usos legítimos — coordenação entre sistemas, testes que precisam de controle externo sobre resolução. Mas como padrão geral é um smell: indica que a lógica de resolução deveria estar encapsulada na própria promise, não vazada para fora.
7. unhandledRejection em Node.js
Quando uma promise é rejeitada e nenhum handler de rejeição é registrado dentro de um tick do event loop, o Node.js emite o evento unhandledRejection no objeto process:
process.on('unhandledRejection', (reason, promise) => { console.error('Promise rejeitada sem handler:', reason); console.error('Promise:', promise); // Em produção: exportar para o sistema de logging/alertas});
O comportamento padrão evoluiu ao longo das versões do Node:
Versão
Comportamento padrão
Node < 15
Aviso no stderr, processo continua
Node 15+
Termina o processo com exit code não-zero
Node 22+
Termina o processo (mesmo comportamento)
Terminar o processo é o comportamento correto para a maioria dos cenários de produção: uma promise rejeitada sem handler indica um estado inesperado que o código não sabe tratar — continuar rodando nesse estado é perigoso.
O flag --unhandled-rejections permite controlar o comportamento explicitamente:
--unhandled-rejections=none ou engolir o evento sem logar é uma forma garantida de criar bugs fantasma — erros que ocorrem silenciosamente sem deixar rastro. Sempre logue o reason com contexto suficiente para investigação posterior.
Na prática
Normalização de APIs value-or-promise
Libs que aceitam tanto valores síncronos quanto promises usam Promise.resolve() para normalizar:
async function processInput(input) { // input pode ser um valor ou uma promise — não importa const value = await Promise.resolve(input); return transform(value);}
Isso evita que o chamador precise saber se a função retorna síncronamente ou não.
Handler de unhandledRejection em produção
Em aplicações Node.js de produção, o handler global é sempre presente e exporta para o sistema de logging:
process.on('unhandledRejection', (reason, promise) => { logger.error({ event: 'unhandled_rejection', reason: reason instanceof Error ? reason.stack : String(reason), }); // Deixar o processo terminar — o process manager (PM2, systemd) vai reiniciar});
Fire and forget — quando é aceitável
Em casos onde a rejeição é genuinamente irrelevante (operações de melhor esforço, side effects não críticos), void documenta a intenção:
// void comunica que a rejeição foi considerada e deliberadamente ignoradavoid updateAnalyticsCache(userId);// Melhor ainda: trate internamente na própria funçãoasync function updateAnalyticsCache(userId) { try { await cache.set(userId, await fetchAnalytics(userId)); } catch (e) { logger.warn('analytics cache update failed', { userId, error: e }); // Falha silenciosa intencional, com log }}
void sem .catch() interno ainda vai disparar unhandledRejection se a promise rejeitar. O padrão correto é sempre tratar internamente quando fire-and-forget for necessário.
Promise.all([]) e casos extremos
Promise.all([]) .then((results) => console.log(results)); // []// Promise.all falha rápido — rejeita no primeiro erroPromise.all([ Promise.resolve(1), Promise.reject(new Error('falhou')), Promise.resolve(3),]).catch((e) => console.log(e.message)); // 'falhou'// Os outros valores (1, 3) são descartados silenciosamente// Promise.allSettled aguarda todos — nunca rejeitaPromise.allSettled([ Promise.resolve(1), Promise.reject(new Error('falhou')),]).then((results) => console.log(results));// [// { status: 'fulfilled', value: 1 },// { status: 'rejected', reason: Error('falhou') }// ]
Casos práticos
Cenário 1 — Promise solta em cadeia de .then(): dados incorretos silenciosos
Um endpoint de usuário buscava permissões mas a lista sempre chegava vazia. Sem erro, sem log — simplesmente undefined.
// ❌ Promise solta: fetchPermissions executa, mas resultado é descartadofetchUser(id) .then((user) => { fetchPermissions(user.id); // sem return! — a promise não é encadeada }) .then((permissions) => { console.log(permissions); // undefined sempre — não as permissões res.json({ permissions }); // resposta incorreta, sem erro });// ✅ Com return: a cadeia espera fetchPermissions resolverfetchUser(id) .then((user) => { return fetchPermissions(user.id); // encadeia corretamente }) .then((permissions) => { res.json({ permissions }); // dados corretos });
Qualquer erro dentro de fetchPermissions no código incorreto vira um unhandledRejection silencioso — especialmente perigoso em Node.js 14 e anteriores (que não encerravam o processo por padrão).
Cenário 2 — Promise.all vs Promise.allSettled em chamadas a múltiplos serviços
Um dashboard agregava dados de 5 serviços externos. Um dos serviços ficava instável, derrubando toda a resposta do dashboard.
// ❌ Promise.all: falha rápida — um serviço instável derruba o dashboard inteiroconst [usuarios, pedidos, estoque, financeiro, analytics] = await Promise.all([ buscarUsuarios(), buscarPedidos(), buscarEstoque(), buscarFinanceiro(), // se este rejeitar, todos os outros são ignorados buscarAnalytics(),]);// ✅ Promise.allSettled: cada serviço contribui independentementeconst resultados = await Promise.allSettled([ buscarUsuarios(), buscarPedidos(), buscarEstoque(), buscarFinanceiro(), // rejeição aqui não cancela os outros buscarAnalytics(),]);const dados = resultados.reduce((acc, r, i) => { if (r.status === 'fulfilled') acc[SERVICOS[i]] = r.value; else console.warn(`${SERVICOS[i]} falhou:`, r.reason); return acc;}, {});
Armadilhas comuns
Esquecer return dentro de .then() cria promise solta — erros desaparecem silenciosamente
Cada .then() retorna uma nova promise cujo estado depende do que o handler retorna. Sem return, a promise interna executa mas o resultado é descartado — a cadeia continua com undefined, e erros viram unhandledRejection invisíveis.
await Promise.resolve(syncFn()) não move trabalho para outra thread
syncFn() executa síncronamente antes mesmo do await. Envolver em Promise.resolve() não torna a função assíncrona — o await só suspende depois que o trabalho síncrono já bloqueou a thread.
// ❌ Bloqueia a thread antes do awaitconst result = await Promise.resolve(computeHeavySync()); // computeHeavySync roda agora// ✅ Para trabalho CPU-bound real: Worker Threads
Silenciar unhandledRejection sem logar cria bugs fantasma em produção
“Tratar” o evento sem logar é pior que não tratar: o processo não termina (pre-Node 15), mas os erros também não aparecem em nenhum lugar. Bugs se acumulam silenciosamente.
// ❌ Engole erros — versão mais perigosaprocess.on('unhandledRejection', () => { /* nada */ });// ✅ Sempre logar; em produção, considere terminar o processoprocess.on('unhandledRejection', (reason, promise) => { logger.error({ reason, promise }, 'Unhandled rejection'); process.exit(1); // Node 15+ faz isso por padrão});
Em entrevista
Frase pronta (use em inglês)
“A Promise has three states: pending, fulfilled, and rejected — once settled, it’s immutable. .then doesn’t run synchronously; it queues the callback in the microtask queue, even if the promise is already resolved. Chained .then calls return new promises, so errors propagate down the chain until the first .catch. The unhandled rejection handler in Node logs the error and, by default in modern versions, terminates the process — which is the right behavior for unrecoverable bugs.”
Perguntas frequentes
“Por que .then() é sempre assíncrono, mesmo para promises já resolvidas?”
Para garantir comportamento previsível e composição correta. Se .then() às vezes rodasse síncronamente, o código teria que lidar com dois modelos de execução dependendo do estado da promise — exatamente o problema que Promises foram criadas para resolver.
“Qual a diferença entre Promise.resolve(p) onde p já é uma Promise?”
Se p é uma Promise nativa, Promise.resolve(p) retorna p sem criar uma nova promise. Se p é um thenable de terceiros, cria uma nova promise que segue p.
“Quando usar Promise.allSettled em vez de Promise.all?”Promise.all é para “preciso de todos os resultados ou quero falhar rápido”. Promise.allSettled é para “quero saber o resultado de cada operação independentemente, sem que uma falha cancele as outras”.
Vocabulário para entrevistas
PT-BR
EN
estado (pending, fulfilled, etc.)
state / status
liquidada / resolvida
settled
encadear
chain / pipe
rejeição não tratada
unhandled rejection
fila de microtasks
microtask queue / job queue
executor
executor function
propagar erro
propagate / bubble up
açúcar sintático
syntactic sugar
promessa solta
floating promise
O que vem a seguir
Com a máquina de estados de Promises entendida, o próximo passo é ver como async/await simplifica o trabalho com Promises — e quais armadilhas surgem da confusão entre a sintaxe mais simples e o modelo de execução subjacente que não mudou.
A nota 09 - async-await - o que é, o que não é desfaz o mito de que async paraleliza ou cria threads, e mostra padrões como Promise.all para paralelismo real e Worker Threads para CPU-bound.