jonathanjuliani
Promises em JavaScript: do zero ao async/await
Promises e async/await em JavaScript do zero: estados, encadeamento, tratamento de erros e os gotchas que a documentação oficial passa batido.
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.raceePromise.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.
O problema que Promises vieram resolver
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:
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:
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:
fetch('https://api.exemplo.com/usuario/1')
.then(resposta => resposta.json()) // transforma Response em objeto
.then(usuario => console.log(usuario.nome)); // usa o resultadoPerceba 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:
fetch('https://api.exemplo.com/usuario/1')
.then(resposta => resposta.json())
.then(usuario => console.log(usuario.nome))
.catch(erro => console.error('falhou:', erro.message));Um .catch() no final captura erros de qualquer .then() anterior — você não precisa de um por .then().
.finally() executa sempre, independente do resultado. Perfeito pra limpar estado, esconder loading, fechar conexão:
function buscarDados() {
setCarregando(true);
return fetch('https://api.exemplo.com/dados')
.then(resposta => resposta.json())
.then(dados => setDados(dados))
.catch(erro => setErro(erro.message))
.finally(() => setCarregando(false)); // sempre executa
}"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:
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:
async function buscarUsuario(id) {
const resposta = await fetch(`https://api.exemplo.com/usuario/${id}`);
const usuario = await resposta.json();
return usuario;
}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:
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
awaitfora de umaasyncfunction?"
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:
// ERRADO — map não espera as Promises resolverem
const resultados = ids.map(async (id) => await buscarUsuario(id));
// resultados é um array de Promises, não de usuários
// CERTO — aguarda todas
const 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:
async function buscarPagina(usuarioId, pedidoId) {
const [usuario, pedidos] = await Promise.all([
buscarUsuario(usuarioId),
buscarPedidos(pedidoId),
]);
return { usuario, pedidos };
}Muito mais eficiente que duas chamadas sequenciais — as duas requisições rodam em paralelo.
Promise.race — a primeira que chegar
Resolve ou rejeita com o resultado da primeira Promise que completar:
const resultado = await Promise.race([
buscarDados(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), 5000)
),
]);Ú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:
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:
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:
// ERRADO — nenhuma das Promises tem tratamento de erro
ids.forEach(id => {
buscarUsuario(id); // dispara mas não espera, não trata
});
// CERTO
await 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:
registrarEvento(dados).catch(() => {}); // dispara sem esperar, ignora o erro conscientementeSó 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 |
Promise.all | Paralelo, falha se qualquer um falhar |
Promise.allSettled | Paralelo, aguarda todos, relata cada resultado |
Promise.race | Retorna o primeiro que completar |
for await...of | Itera sobre iterables assíncronos em sequência |
await no .map() | Não faz o que parece — use Promise.all |
Próximos passos
Se você chegou até aqui pra entender Promises antes de ver o ES9 — faz sentido. O for await...of e o Promise.finally() ficam mais claros quando você vê eles no contexto das outras features do ES7 ao ES10: ECMAScript ES7 ao ES10: as features que você usa sem saber de onde vieram.
E se quiser entender como módulos funcionam no Node.js antes de misturar require com código assíncrono, o Node.js: require, exports e module.exports explicados cobre isso sem enrolação.
[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"]
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.
Artigos relacionados
Spread e Rest em JavaScript: arrays, objetos e os gotchas de cópia rasa
Spread operator e rest em JavaScript: como funcionam em arrays, objetos e funções, e os gotchas de cópia rasa que todo dev encontra.
Ler publicação
Implementando Busca Binária com NodeJS e Javascript
Busca binária em JavaScript: versões iterativa e recursiva, array ordenado e complexidade O(log n). Fechamento da série com código.
Ler publicação