Fundamentos do Node.js
Promises: Uma Abordagem Moderna para Assincronismo
Aprenda sobre promises: uma abordagem moderna para assincronismo
Promises: Uma Abordagem Moderna para Assincronismo
Olá, futuros ninjas do Node.js! 👋 Nesta aula, vamos mergulhar em um dos pilares da programação assíncrona moderna em JavaScript e Node.js: as Promises. Elas vieram para simplificar e tornar mais legível o código que lida com operações que não acontecem instantaneamente, como leitura de arquivos, requisições de rede ou acesso a bancos de dados.
1. Introdução: O Problema e a Solução 💡
No mundo do JavaScript, muitas operações são inerentemente assíncronas. Isso significa que elas iniciam agora, mas terminam em algum momento no futuro, sem bloquear a execução do restante do seu código. Tradicionalmente, lidávamos com isso usando callbacks: funções que eram passadas como argumento e executadas quando a operação assíncrona terminava.
// Exemplo clássico de callback hell (inferno de callbacks)
fs.readFile('arquivo1.txt', 'utf8', (err, data1) => {
if (err) throw err;
console.log('Conteúdo do arquivo 1:', data1);
fs.readFile('arquivo2.txt', 'utf8', (err, data2) => {
if (err) throw err;
console.log('Conteúdo do arquivo 2:', data2);
db.query('SELECT * FROM users', (err, users) => {
if (err) throw err;
console.log('Usuários do banco de dados:', users);
// ... e assim por diante, aninhando callbacks
});
});
});Esse padrão, conhecido como "callback hell" ou "pirâmide da perdição", torna o código difícil de ler, manter e depurar. 😱
É aqui que as Promises entram em cena! Elas fornecem uma maneira mais estruturada e elegante de lidar com operações assíncronas, transformando a "pirâmide" em uma "cadeia" legível.
Uma Promise é um objeto que representa a eventual conclusão (ou falha) de uma operação assíncrona e seu valor resultante. Pense nela como uma promessa da vida real: você faz uma promessa de que algo será feito. Essa promessa pode ser cumprida (resolvida) ou quebrada (rejeitada).
2. Explicação Detalhada com Exemplos ✨
2.1. O que são Promises? Estados de uma Promise
Uma Promise pode estar em um dos três estados:
pending(pendente): Estado inicial. A operação assíncrona ainda não foi concluída nem falhou.fulfilled(resolvida/cumprida): A operação foi concluída com sucesso. O valor resultante está disponível.rejected(rejeitada): A operação falhou. O motivo da falha (um erro) está disponível.
Uma vez que uma Promise é resolvida ou rejeitada, ela se torna settled (finalizada) e seu estado não pode mais mudar.
2.2. Criando uma Promise 🛠️
Você pode criar uma Promise usando o construtor new Promise(). Ele recebe uma função executora como argumento, que por sua vez recebe duas funções como parâmetros: resolve e reject.
resolve(value): Chame esta função quando a operação assíncrona for bem-sucedida, passando o valor resultante.reject(reason): Chame esta função quando a operação assíncrona falhar, passando o motivo da falha (geralmente um objetoError).
// Exemplo: Uma Promise que resolve após 2 segundos
const minhaPrimeiraPromise = new Promise((resolve, reject) => {
console.log('Promise pendente: Aguardando 2 segundos...');
setTimeout(() => {
const sucesso = Math.random() > 0.5; // 50% de chance de sucesso
if (sucesso) {
resolve('Dados carregados com sucesso!'); // Resolve a promise
} else {
reject(new Error('Falha ao carregar os dados.')); // Rejeita a promise
}
}, 2000);
});
console.log(minhaPrimeiraPromise); // Estado inicial: Promise { <pending> }2.3. Consumindo uma Promise: .then(), .catch(), .finally() 🎣
Para lidar com o resultado de uma Promise, usamos os métodos .then(), .catch() e .finally().
.then(onFulfilled, onRejected):onFulfilled: Uma função que é chamada quando a Promise é resolvida. Recebe o valor deresolve().onRejected: (Opcional) Uma função que é chamada quando a Promise é rejeitada. Recebe o valor dereject(). É mais comum usar.catch()para tratamento de erros.
.catch(onRejected):- Uma função que é chamada quando a Promise é rejeitada. É um atalho para
.then(null, onRejected). É a forma preferida para lidar com erros.
- Uma função que é chamada quando a Promise é rejeitada. É um atalho para
.finally(onFinally):- Uma função que é chamada quando a Promise é finalizada (resolvida ou rejeitada). Útil para limpeza, como fechar conexões ou remover loaders, independentemente do resultado.
minhaPrimeiraPromise
.then((mensagemSucesso) => {
console.log('✅ Sucesso:', mensagemSucesso);
return 'Processamento concluído!'; // O valor retornado aqui será passado para o próximo .then()
})
.then((mensagemProximoPasso) => {
console.log('➡️ Próximo passo:', mensagemProximoPasso);
})
.catch((erro) => {
console.error('❌ Erro:', erro.message);
// Erros podem ser tratados e até mesmo recuperados, retornando um novo valor
// return 'Valor padrão após erro';
})
.finally(() => {
console.log('✨ Operação finalizada, independentemente do resultado.');
});
// Exemplo com Promise que sempre rejeita para testar o catch
const promiseQueFalha = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Erro intencional!')), 1000);
});
promiseQueFalha
.then(data => console.log('Nunca será executado', data))
.catch(error => console.error('Capturado pelo catch:', error.message))
.finally(() => console.log('Sempre executado'));2.4. Cadeia de Promises (Promise Chaining) 🔗
A beleza das Promises reside na sua capacidade de serem encadeadas. Quando você retorna uma Promise de dentro de um .then(), o próximo .then() (ou .catch()) esperará que essa nova Promise seja resolvida antes de ser executado. Isso permite sequenciar operações assíncronas de forma linear e legível.
function buscarUsuario(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 1) {
resolve({ id: 1, nome: 'Alice' });
} else {
reject(new Error('Usuário não encontrado.'));
}
}, 1000);
});
}
function buscarPedidos(usuarioId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (usuarioId === 1) {
resolve([{ id: 101, produto: 'Livro' }, { id: 102, produto: 'Caneta' }]);
} else {
reject(new Error('Pedidos não encontrados para este usuário.'));
}
}, 1500);
});
}
buscarUsuario(1)
.then(usuario => {
console.log('Usuário encontrado:', usuario.nome);
return buscarPedidos(usuario.id); // Retorna uma nova Promise
})
.then(pedidos => {
console.log('Pedidos do usuário:', pedidos);
console.log('Processo completo!');
})
.catch(erro => {
console.error('Ocorreu um erro na cadeia:', erro.message);
})
.finally(() => {
console.log('Finalizando a busca...');
});
// Teste com um ID que não existe
buscarUsuario(2)
.then(usuario => buscarPedidos(usuario.id))
.then(pedidos => console.log(pedidos))
.catch(erro => console.error('Erro ao buscar usuário ou pedidos:', erro.message));3. Código de Exemplo Oficial: fs.promises no Node.js 📄
O Node.js modernizou muitas de suas APIs de callback para usar Promises. O módulo fs (File System) é um excelente exemplo, oferecendo uma versão baseada em Promise chamada fs.promises.
Este exemplo é adaptado da documentação oficial do Node.js para fs.promises.
import { readFile, writeFile } from 'node:fs/promises'; // Importa as versões Promise-based
async function operarComArquivos() {
const nomeArquivoEntrada = 'dados.txt';
const nomeArquivoSaida = 'dados_processados.txt';
try {
// 1. Criar um arquivo de entrada (se não existir)
await writeFile(nomeArquivoEntrada, 'Olá, mundo!\nEsta é uma linha de teste.');
console.log(`Arquivo '${nomeArquivoEntrada}' criado com sucesso.`);
// 2. Ler o conteúdo do arquivo
const conteudo = await readFile(nomeArquivoEntrada, 'utf8');
console.log(`Conteúdo lido de '${nomeArquivoEntrada}':\n${conteudo}`);
// 3. Processar o conteúdo (ex: converter para maiúsculas)
const conteudoProcessado = conteudo.toUpperCase();
// 4. Escrever o conteúdo processado em um novo arquivo
await writeFile(nomeArquivoSaida, conteudoProcessado);
console.log(`Conteúdo processado salvo em '${nomeArquivoSaida}'.`);
// 5. Ler o arquivo de saída para verificar
const conteudoVerificado = await readFile(nomeArquivoSaida, 'utf8');
console.log(`Conteúdo verificado de '${nomeArquivoSaida}':\n${conteidoVerificado}`);
} catch (err) {
console.error('❌ Ocorreu um erro durante a operação de arquivos:', err.message);
}
}
operarComArquivos();Nota: O
awaité uma sintaxe açúcar para Promises e será abordado em detalhes na próxima aula. Por enquanto, entenda queawait"espera" a Promise resolver e retorna seu valor, ou lança um erro se ela for rejeitada. Semawait, você usaria.then().catch().
4. Métodos Estáticos de Promise 🚀
A classe Promise oferece alguns métodos estáticos úteis para lidar com múltiplas Promises simultaneamente.
4.1. Promise.all(iterable)
Espera que todas as Promises no iterable (geralmente um array) sejam resolvidas. Se todas resolverem, ele retorna uma nova Promise que resolve com um array dos valores resultantes, na mesma ordem em que foram passadas. Se qualquer uma das Promises for rejeitada, Promise.all rejeita imediatamente com o erro da primeira Promise rejeitada.
const promise1 = Promise.resolve(3);
const promise2 = 42; // Não é uma Promise, é tratada como uma Promise resolvida
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log('✅ Promise.all - Todos resolvidos:', values); // [3, 42, "foo"]
})
.catch((error) => {
console.error('❌ Promise.all - Erro:', error);
});
const promiseRejeitada1 = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Erro na P1')), 50));
const promiseRejeitada2 = new Promise((resolve, reject) => setTimeout(() => resolve('P2 Sucesso'), 100));
Promise.all([promiseRejeitada1, promiseRejeitada2])
.then((values) => console.log('Isso não será executado:', values))
.catch((error) => {
console.error('❌ Promise.all - Erro capturado (primeiro erro):', error.message); // Erro na P1
});4.2. Promise.race(iterable)
Espera que a primeira Promise no iterable a resolver ou rejeitar seja finalizada. Retorna uma nova Promise que resolve ou rejeita com o resultado dessa primeira Promise.
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve('Primeiro!'), 500);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Eu sou mais rápido, mas falhei!')), 100);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => resolve('Segundo!'), 1000);
});
Promise.race([p1, p2, p3])
.then((value) => {
console.log('✅ Promise.race - Vencedor:', value); // Nunca será executado
})
.catch((error) => {
console.error('❌ Promise.race - Vencedor (erro):', error.message); // Eu sou mais rápido, mas falhei!
});
// Exemplo com sucesso
const p4 = new Promise((resolve, reject) => { setTimeout(() => resolve('Primeiro sucesso!'), 200); });
const p5 = new Promise((resolve, reject) => { setTimeout(() => reject(new Error('Erro lento')), 500); });
Promise.race([p4, p5])
.then((value) => console.log('✅ Promise.race - Vencedor:', value)) // Primeiro sucesso!
.catch((error) => console.error('❌ Promise.race - Vencedor (erro):', error.message));4.3. Promise.allSettled(iterable)
Espera que todas as Promises no iterable sejam finalizadas (resolvidas ou rejeitadas). Retorna uma Promise que resolve com um array de objetos, cada um descrevendo o resultado de cada Promise (se foi fulfilled ou rejected, e qual o valor/razão). É útil quando você não se importa se uma Promise falha, apenas quer saber o resultado de todas.
const promiseSucesso = Promise.resolve('Sucesso!');
const promiseFalha = Promise.reject(new Error('Falha!'));
const promiseLenta = new Promise((resolve) => setTimeout(() => resolve('Lenta mas vai!'), 300));
Promise.allSettled([promiseSucesso, promiseFalha, promiseLenta])
.then((results) => {
console.log('✅ Promise.allSettled - Todos finalizados:', results);
/* Saída esperada:
[
{ status: 'fulfilled', value: 'Sucesso!' },
{ status: 'rejected', reason: [Error: Falha!] },
{ status: 'fulfilled', value: 'Lenta mas vai!' }
]
*/
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(` Promise ${index + 1} - ✅ Resolvida:`, result.value);
} else {
console.error(` Promise ${index + 1} - ❌ Rejeitada:`, result.reason.message);
}
});
});4.4. Promise.any(iterable)
Espera que a primeira Promise no iterable a resolver seja finalizada. Se todas as Promises forem rejeitadas, ele rejeita com um AggregateError que contém os erros de todas as Promises rejeitadas.
const pA = new Promise((resolve, reject) => setTimeout(() => reject(new Error('A falhou')), 100));
const pB = new Promise((resolve, reject) => setTimeout(() => resolve('B sucesso!'), 200));
const pC = new Promise((resolve, reject) => setTimeout(() => resolve('C sucesso!'), 300));
Promise.any([pA, pB, pC])
.then((value) => {
console.log('✅ Promise.any - Primeira a resolver:', value); // B sucesso!
})
.catch((error) => {
console.error('❌ Promise.any - Todas falharam:', error.message);
});
const pD = new Promise((resolve, reject) => setTimeout(() => reject(new Error('D falhou')), 100));
const pE = new Promise((resolve, reject) => setTimeout(() => reject(new Error('E falhou')), 200));
Promise.any([pD, pE])
.then((value) => console.log('Nunca será executado:', value))
.catch((error) => {
console.error('❌ Promise.any - Todas falharam:', error.errors.map(e => e.message)); // [ 'D falhou', 'E falhou' ]
});5. Perguntas para Reflexão (Teoria) 🤔
Como esta é uma aula teórica, em vez de exercícios de codificação, convido você a refletir sobre as seguintes questões para solidificar seu entendimento:
- Qual é a principal motivação para usar Promises em vez de callbacks aninhados? Quais problemas as Promises resolvem?
- Descreva os três estados de uma Promise. Uma Promise pode mudar de estado de
fulfilledpararejected? Por quê? - Quando você usaria
Promise.all()versusPromise.race()? Dê um exemplo de cenário para cada um. - Qual a diferença fundamental entre
Promise.all()ePromise.allSettled()? Em que situaçãoPromise.allSettled()seria mais apropriado? - Explique como a "cadeia de Promises" (
Promise Chaining) funciona e como ela contribui para um código mais legível.
6. Resumo e Próximos Passos 🚀
Nesta aula, exploramos as Promises, uma ferramenta fundamental para lidar com o assincronismo de forma eficaz no JavaScript e Node.js. Vimos:
- A necessidade das Promises para evitar o "callback hell".
- Os estados de uma Promise:
pending,fulfilled,rejected. - Como criar e consumir Promises usando
.then(),.catch()e.finally(). - O poder da cadeia de Promises para sequenciar operações assíncronas.
- Exemplos práticos com
fs.promisesdo Node.js. - Métodos estáticos avançados como
Promise.all(),Promise.race(),Promise.allSettled()ePromise.any()para orquestrar múltiplas operações assíncronas.
As Promises transformaram a maneira como escrevemos código assíncrono, tornando-o mais legível, robusto e fácil de manter. Elas são a base para a próxima grande evolução do assincronismo em JavaScript: async/await.
Próximos Passos: Na próxima aula, vamos desvendar async/await, a sintaxe açúcar que torna o trabalho com Promises ainda mais parecido com o código síncrono, elevando a legibilidade a um novo patamar! Prepare-se para uma experiência de codificação ainda mais fluida! ✨