Promises e async/await em JavaScript do zero: estados, encadeamento, tratamento de erros e os gotchas que a documentação oficial passa batido.
Jonathan Juliani
Engenheiro de Software Sr - Full Stack · WithClutch Inc. | Juliani Tecnologia
JavaScript
Você escreve uma função que busca dados de uma API, loga o resultado no console — e aparece undefined. O código parece certo. A URL está certa. Mas o valor nunca chegou a tempo. Bora resolver isso de vez.
Promises em JavaScript são o mecanismo que permite trabalhar com operações assíncronas sem transformar o código numa pirâmide de callbacks. Entender como elas funcionam de verdade — não só a sintaxe, mas os estados e os erros silenciosos — é o que separa quem usa async/await de quem entende o que está acontecendo por baixo.
O que você vai aprender:
O que é uma Promise e quais são os três estados possíveis
Como usar .then(), .catch() e .finally() encadeados
Como async/await é só açúcar sintático em cima de Promise
Quando usar Promise.all, Promise.race e Promise.allSettled
for await...of — o que é e onde encaixa de verdade
Os gotchas que a documentação oficial passa batido
Pré-requisitos: funções, arrays, objetos e noções de JavaScript básico. Se quiser entender como o Node organiza módulos antes de usar fetch e require em conjunto, vale passar por Node.js: require, exports e module.exports explicados primeiro.
Inscreva-se no Ledger da Engenharia
Receba notas de arquitetura e performance na sua caixa de entrada. Mesma lista do convite—inscreva-se aqui quando quiser.
JavaScript roda em uma única thread. Quando você faz uma requisição de rede, lê um arquivo ou consulta um banco, o motor JS não pode simplesmente pausar e esperar — isso travaria tudo. A solução histórica era callbacks: você passa uma função pra ser chamada quando o resultado chegar.
O problema aparece quando você precisa de vários resultados em sequência:
código
buscarUsuario(id, function(erro, usuario) { if (erro) return console.error(erro); buscarPedidos(usuario.id, function(erro, pedidos) { if (erro) return console.error(erro); buscarProduto(pedidos[0].produtoId, function(erro, produto) { if (erro) return console.error(erro); // finalmente aqui console.log(produto.nome); }); });});
Isso tem nome: callback hell. O código cresce pra direita, o tratamento de erro se repete em cada nível e qualquer refatoração vira pesadelo. Promises foram a resposta do ES6 pra esse problema.
Promise: o objeto e os três estados
Uma Promise representa um valor que pode não estar disponível ainda. Ela sempre está em um de três estados:
pending — operação em andamento, resultado ainda não chegou
fulfilled — operação concluída com sucesso, valor disponível
rejected — operação falhou, motivo do erro disponível
Um estado resolvido ou rejeitado é final — uma Promise não volta pra pending e não muda de fulfilled pra rejected.
[IMAGEM: diagrama de fluxo mostrando os três estados de uma Promise — pending no centro, seta para fulfilled à direita e seta para rejected à esquerda, com os rótulos resolve() e reject() | alt: "Ciclo de vida de uma Promise em JavaScript: estados pending, fulfilled e rejected"]
Você cria uma Promise assim:
código
const promessa = new Promise((resolve, reject) => { const deuCerto = true; // simula o resultado de uma operação if (deuCerto) { resolve('operação concluída'); // move pra fulfilled } else { reject(new Error('algo deu errado')); // move pra rejected }});
O construtor recebe uma função executora com dois parâmetros: resolve (chama quando deu certo, com o valor) e reject (chama quando falhou, normalmente com um Error).
Na prática você raramente cria Promises do zero assim — você usa APIs que já retornam Promise, como fetch, fs.promises.readFile ou qualquer biblioteca moderna. Mas entender o construtor é o que faz o resto fazer sentido.
.then(), .catch() e .finally(): encadeando sem perder o fio
.then() recebe uma função que será chamada quando a Promise for fulfilled, com o valor resolvido como argumento:
código
fetch('https://api.exemplo.com/usuario/1') .then(resposta => resposta.json()) // transforma Response em objeto .then(usuario => console.log(usuario.nome)); // usa o resultado
Perceba o encadeamento: .then() retorna uma nova Promise. O que você retornar dentro do callback vira o valor da próxima Promise na cadeia. Isso é o que permite encadear sem aninhar.
.catch() captura qualquer rejeição que aconteceu na cadeia acima dele:
"Tá Jon, mas o que acontece se eu esquecer o .catch()?"
Se uma Promise é rejeitada e não tem nenhum handler de erro na cadeia, você vai levar um UnhandledPromiseRejection — que no Node.js encerra o processo por padrão desde a versão 15. No browser, vira um erro no console que pode passar despercebido. Sempre trate o erro.
async/await: Promise com cara de código síncrono
async/await é açúcar sintático em cima de Promises — não um mecanismo diferente. Uma função async sempre retorna uma Promise, mesmo que você retorne um valor simples:
código
async function saudacao() { return 'oi'; // equivale a Promise.resolve('oi')}saudacao().then(console.log); // 'oi'
await pausa a execução da função assíncrona até a Promise resolver, e extrai o valor:
Muito mais legível que o encadeamento de .then(). O mesmo código, a mesma Promise por baixo.
O tratamento de erro com async/await é com try/catch:
código
async function buscarUsuario(id) { try { const resposta = await fetch(`https://api.exemplo.com/usuario/${id}`); if (!resposta.ok) { throw new Error(`HTTP ${resposta.status}`); // joga manualmente pra cair no catch } return await resposta.json(); } catch (erro) { console.error('falha ao buscar usuário:', erro.message); throw erro; // re-lança pra quem chamou tratar também }}
"Tá Jon, e se eu usar await fora de uma async function?"
Erro de sintaxe. await só funciona dentro de async. Exceção: no nível raiz de módulos ES (type: "module" no package.json ou arquivo .mjs) você pode usar top-level await — mas em CommonJS tradicional, não.
Uma coisa que pega bastante: await dentro de .map() não faz o que parece:
código
// ERRADO — map não espera as Promises resolveremconst resultados = ids.map(async (id) => await buscarUsuario(id));// resultados é um array de Promises, não de usuários// CERTO — aguarda todasconst resultados = await Promise.all(ids.map(id => buscarUsuario(id)));
Quando tem mais de uma: Promise.all, race e allSettled
Quando você precisa de múltiplas operações assíncronas, tem três ferramentas principais.
Promise.all — todas ou nenhuma
Recebe um array de Promises e retorna uma Promise que resolve com um array de resultados — mas rejeita se qualquer uma rejeitar:
Útil pra implementar timeouts: se buscarDados demorar mais que 5 segundos, a Promise de timeout rejeita primeiro.
Promise.allSettled — aguarda todas, sem curto-circuito
Diferente do Promise.all, não rejeita se uma falhar. Aguarda todas terminarem e retorna um array descrevendo cada resultado:
código
const resultados = await Promise.allSettled([ buscarUsuario(1), buscarUsuario(99999), // esse vai falhar buscarUsuario(3),]);resultados.forEach(resultado => { if (resultado.status === 'fulfilled') { console.log('ok:', resultado.value.nome); } else { console.error('falhou:', resultado.reason.message); }});
Use allSettled quando uma falha parcial é aceitável — como carregar múltiplos widgets de um dashboard.
Método
Comportamento em falha
Quando usar
Promise.all
Rejeita na primeira falha
Todas as respostas são obrigatórias
Promise.race
Resolve/rejeita com a primeira
Timeout, cache vs. rede
Promise.allSettled
Aguarda todas, relata cada uma
Falha parcial é aceitável
for await...of e o que a doc não fala
for await...of é do ES2018 e permite iterar sobre iterables assíncronos — streams, generators assíncronos, qualquer coisa que retorna Promises em sequência:
código
async function processarLinhas(stream) { for await (const linha of stream) { await processarLinha(linha); // aguarda cada uma antes de continuar }}
É diferente de Promise.all: o for await...of processa em sequência, um por vez. O Promise.all processa em paralelo. A escolha depende se a ordem importa ou se uma depende do resultado da outra.
Gotcha: UnhandledPromiseRejection
[IMAGEM: screenshot do terminal mostrando o erro "UnhandledPromiseRejectionWarning" no Node.js | alt: "Erro UnhandledPromiseRejection no Node.js quando uma Promise é rejeitada sem handler"]
Esse erro aparece quando você cria ou encadeia uma Promise, ela rejeita, e não tem nenhum .catch() ou try/catch pra capturar. Muito comum em loops:
código
// ERRADO — nenhuma das Promises tem tratamento de erroids.forEach(id => { buscarUsuario(id); // dispara mas não espera, não trata});// CERTOawait Promise.all( ids.map(async (id) => { try { return await buscarUsuario(id); } catch (erro) { console.error(`falha no id ${id}:`, erro.message); return null; } }));
"Tá Jon, e se eu quiser disparar uma Promise sem esperar o resultado — tipo um fire-and-forget?"
Você pode, mas adicione um .catch() vazio no mínimo pra não levar UnhandledPromiseRejection:
código
registrarEvento(dados).catch(() => {}); // dispara sem esperar, ignora o erro conscientemente
Só faça isso se você realmente não se importa com o resultado. Em produção, pelo menos logue o erro.
TL;DR
Conceito
O que lembrar
Promise
Representa um valor futuro. Estados: pending → fulfilled ou rejected
.then()
Recebe o valor resolvido; retorna nova Promise
.catch()
Captura qualquer rejeição na cadeia acima
.finally()
Sempre executa; não recebe valor, não altera resultado
async
Faz a função retornar Promise automaticamente
await
Pausa a função async até a Promise resolver; extrai o valor
[PREENCHER: adicionar CTA personalizado aqui — pode ser algo como "Como você lida com tratamento de erro em chamadas assíncronas em produção? Tem algum padrão que descobriu na prática que não vi aqui? Me fala nos comentários — se viu alguma cagada nesse post também, corrige aí :D"]