Promises: resolvendo o callback hell
1. O problema do callback hell
Se você já trabalhou com JavaScript assíncrono por algum tempo, provavelmente esbarrou no temido callback hell — também conhecido como "pyramid of doom". Esse padrão surge quando múltiplas operações assíncronas dependem umas das outras, criando níveis profundos de aninhamento que tornam o código praticamente ilegível.
Vejamos um exemplo clássico em Node.js com leitura de arquivos:
const fs = require('fs');
fs.readFile('usuario.json', 'utf8', (err, data) => {
if (err) {
console.error('Erro ao ler usuário:', err);
return;
}
const usuario = JSON.parse(data);
fs.readFile(`pedidos_${usuario.id}.json`, 'utf8', (err, data) => {
if (err) {
console.error('Erro ao ler pedidos:', err);
return;
}
const pedidos = JSON.parse(data);
fs.readFile(`detalhes_${pedidos[0].id}.json`, 'utf8', (err, data) => {
if (err) {
console.error('Erro ao ler detalhes:', err);
return;
}
const detalhes = JSON.parse(data);
console.log('Detalhes do pedido:', detalhes);
});
});
});
Os impactos desse padrão são severos:
- Legibilidade comprometida: o código se expande para a direita, dificultando o acompanhamento do fluxo
- Manutenção complexa: adicionar ou modificar etapas exige reestruturar todo o aninhamento
- Tratamento de erros repetitivo: cada callback precisa verificar err manualmente, levando a duplicação e possíveis omissões
2. Introdução às Promises
Uma Promise é um objeto que representa a eventual conclusão (ou falha) de uma operação assíncrona e seu valor resultante. Em vez de passar callbacks para funções, você recebe uma Promise que pode ser "consumida" posteriormente.
Uma Promise pode estar em três estados:
- pending: estado inicial, a operação ainda não foi concluída
- fulfilled: a operação foi concluída com sucesso
- rejected: a operação falhou
Sintaxe básica de criação:
const minhaPromise = new Promise((resolve, reject) => {
const sucesso = true;
setTimeout(() => {
if (sucesso) {
resolve('Operação concluída!');
} else {
reject(new Error('Algo deu errado'));
}
}, 1000);
});
3. Consumindo Promises: then, catch, finally
Para consumir uma Promise, utilizamos três métodos fundamentais:
minhaPromise
.then((resultado) => {
console.log('Sucesso:', resultado);
return 'Próximo valor';
})
.catch((erro) => {
console.error('Erro:', erro.message);
})
.finally(() => {
console.log('Sempre executa, independente do resultado');
});
.then(): recebe o valor resolvido e permite encadeamento.catch(): captura qualquer erro na cadeia.finally(): executado sempre, útil para limpeza ou finalização de loading states
4. Transformando callbacks em Promises
O Node.js oferece util.promisify() para converter funções baseadas em callback em funções que retornam Promises:
const util = require('util');
const fs = require('fs');
const readFileAsync = util.promisify(fs.readFile);
readFileAsync('arquivo.txt', 'utf8')
.then((conteudo) => console.log(conteudo))
.catch((err) => console.error('Erro:', err));
Também podemos criar wrappers manualmente para maior controle:
function readFilePromise(caminho, encoding) {
return new Promise((resolve, reject) => {
fs.readFile(caminho, encoding, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
Esse padrão é essencial para refatorar código legado, substituindo callbacks aninhados por cadeias lineares de Promises.
5. Encadeamento de Promises
O verdadeiro poder das Promises está no encadeamento. Cada .then() pode retornar um valor ou uma nova Promise, permitindo sequenciar operações de forma linear:
readFileAsync('usuario.json', 'utf8')
.then((data) => {
const usuario = JSON.parse(data);
return readFileAsync(`pedidos_${usuario.id}.json`, 'utf8');
})
.then((data) => {
const pedidos = JSON.parse(data);
return readFileAsync(`detalhes_${pedidos[0].id}.json`, 'utf8');
})
.then((data) => {
const detalhes = JSON.parse(data);
console.log('Detalhes do pedido:', detalhes);
})
.catch((err) => {
console.error('Erro em qualquer etapa:', err);
});
Comparação visual:
Antes (callback hell):
function1(callback1 -> function2(callback2 -> function3(callback3)))
Depois (encadeamento linear):
promise1.then(promise2).then(promise3).catch(erro)
6. Tratamento avançado de erros com Promises
Em cadeias longas, um único .catch() no final captura erros de qualquer etapa:
operacao1()
.then((res1) => operacao2(res1))
.then((res2) => operacao3(res2))
.then((res3) => operacao4(res3))
.catch((err) => {
// Captura erro de qualquer operação acima
console.error('Falha na cadeia:', err);
});
Erros não capturados em Promises podem gerar o evento unhandledrejection:
process.on('unhandledRejection', (reason, promise) => {
console.error('Promise rejeitada não tratada:', reason);
});
Boas práticas: sempre finalize suas cadeias com .catch(). Em ambientes Node.js modernos, Promises rejeitadas sem tratamento podem encerrar o processo.
7. Promises no contexto React
No React, Promises são amplamente usadas para busca de dados. O padrão típico em componentes funcionais:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
fetch(`https://api.exemplo.com/users/${userId}`, {
signal: abortController.signal
})
.then((response) => {
if (!response.ok) {
throw new Error('Erro na requisição');
}
return response.json();
})
.then((data) => {
setUser(data);
setLoading(false);
})
.catch((err) => {
if (err.name !== 'AbortError') {
setError(err.message);
setLoading(false);
}
});
// Cleanup: aborta requisição se componente desmontar
return () => abortController.abort();
}, [userId]);
if (loading) return <div>Carregando...</div>;
if (error) return <div>Erro: {error}</div>;
return <div>{user.name}</div>;
}
O uso de AbortController previne memory leaks ao cancelar requisições quando o componente é desmontado antes da conclusão.
8. Conclusão e próximos passos
As Promises revolucionaram o tratamento de código assíncrono em JavaScript, transformando o caos do callback hell em cadeias lineares e compreensíveis. Com estados claros (pending, fulfilled, rejected) e métodos como then, catch e finally, você ganha controle total sobre operações assíncronas.
Limitações: embora muito melhores que callbacks, Promises ainda podem ser verbosas para fluxos complexos. Por isso, a evolução natural é o async/await, que oferece uma sintaxe ainda mais limpa — tema do próximo artigo desta série.
Para aprofundar, explore também Promise.all, Promise.race e Promise.allSettled, que permitem trabalhar com múltiplas Promises simultaneamente.
Referências
- MDN Web Docs: Promise — Documentação oficial completa sobre o objeto Promise, incluindo todos os métodos e exemplos práticos.
- Node.js Documentation: util.promisify() — Guia oficial do Node.js sobre a função que converte callbacks em Promises.
- JavaScript.info: Promises, async/await — Tutorial detalhado sobre programação assíncrona em JavaScript, com exemplos progressivos de callbacks a async/await.
- React Documentation: useEffect — Documentação oficial do React sobre o hook useEffect, incluindo boas práticas com Promises e AbortController.
- freeCodeCamp: How to Escape Callback Hell with Promises — Artigo prático demonstrando a refatoração de código com callback hell para Promises, com exemplos reais.
- DigitalOcean: Understanding Promises in JavaScript — Tutorial completo sobre Promises, desde conceitos básicos até encadeamento e tratamento de erros.