Leodario.com

Leodario.com – Tudo sobre Tecnologia

Aula 8 – API JavaScript, Node.js e Express – Promises Fundamentals – Programação assíncrona

Imagem destacada da aula de API

Introdução

Prezados desenvolvedores e desenvolvedoras, sejam muito bem-vindos à oitava etapa da nossa jornada de especialização em APIs! Hoje, desvendaremos um pilar indispensável para a construção de sistemas modernos e responsivos: as Promises.

Imagine a seguinte cena do mundo real: você está em um restaurante movimentado e faz o seu pedido. O garçom anota, mas ele não fica parado na sua mesa esperando a comida ficar pronta. Em vez disso, ele te entrega um “ticket de pedido”. Você tem uma promessa de que sua refeição será entregue em breve. Enquanto espera, você pode conversar, beber água, navegar no celular. Quando a comida fica pronta, o garçom traz o prato e você o consome.

No universo das APIs e do desenvolvimento Node.js, este “ticket de pedido” é exatamente o que uma Promise representa. Em vez de bloquear todo o sistema esperando uma operação demorada (como consultar um banco de dados, ler um arquivo grande ou fazer uma requisição a outro servidor), o Node.js emite uma promessa de que essa operação será concluída no futuro. Enquanto isso, seu servidor pode continuar atendendo a outros pedidos, garantindo que sua API seja sempre ágil e eficiente.

Dominar as Promises é vital para desenvolver APIs que não travam e que conseguem lidar com múltiplas requisições simultaneamente. Nesta aula, você vai aprender não apenas o que são Promises, mas como construí-las, utilizá-las e gerenciar seus resultados de forma profissional. Isso é fundamental para qualquer aplicação Node.js e Express, pois a maior parte das operações de I/O (Input/Output) nesse ecossistema é assíncrona.

Conceito Fundamental

Tecnicamente, uma Promise (Promessa) em JavaScript e Node.js é um objeto que representa a eventual conclusão (ou falha) de uma operação assíncrona e seu valor resultante. Ela serve como um contêiner para um valor que pode ainda não estar disponível. Pense nela como um placeholder para o resultado de uma tarefa que ainda está em andamento.

Uma Promise pode ter três estados distintos:

    • pending (pendente): O estado inicial, a operação ainda não foi concluída. É como o “ticket de pedido” que você acabou de receber.
    • fulfilled (resolvida): A operação foi concluída com sucesso. A Promise agora tem um valor resultante (o prato chegou com sucesso!).
    • rejected (rejeitada): A operação falhou. A Promise tem uma razão para a falha (o ingrediente acabou e seu prato não pode ser feito).

A terminologia correta da indústria é crucial aqui: quando uma Promise pendente é concluída com sucesso, dizemos que ela foi resolved (resolvida), e quando falha, ela é rejected (rejeitada). Para lidar com esses resultados, utilizamos os métodos .then() para o sucesso, .catch() para o tratamento de erros e .finally() para a execução de código independentemente do resultado.

Casos de uso reais em produção são abundantes. Qualquer operação que leva tempo e não deve bloquear o fluxo principal da aplicação é um candidato perfeito para Promises. Isso inclui:

    • Fazer requisições HTTP para outras APIs externas.
    • Consultar ou gravar dados em bancos de dados (MongoDB, PostgreSQL, MySQL).
    • Ler ou escrever arquivos no sistema de arquivos.
    • Operações que envolvem atrasos de tempo (timers).

As Promises se integram perfeitamente com outras tecnologias e padrões. A Fetch API para requisições HTTP em navegadores, bibliotecas como Axios para Node.js, e a maioria dos drivers de banco de dados modernos já retornam Promises por padrão. Mais adiante em sua jornada, você verá como as Promises são a base para a sintaxe mais legível e poderosa do async/await.

As vantagens de utilizar Promises são significativas:

    • Legibilidade Aprimorada: Reduzem drasticamente o “callback hell” (aninhamento excessivo de funções de callback), tornando o código mais plano e fácil de entender.
    • Tratamento de Erros Centralizado: O método .catch() permite lidar com erros de forma mais organizada e uniforme em toda uma cadeia de operações assíncronas.
    • Encadeamento de Operações: Possibilitam o encadeamento de múltiplas operações assíncronas de forma sequencial, onde o resultado de uma alimenta a próxima.

Contudo, existem algumas desvantagens a serem consideradas:

    • Curva de Aprendizagem Inicial: Para desenvolvedores iniciantes, o conceito de assincronicidade e Promises pode exigir um tempo para ser assimilado.
    • Promessa “Não Utilizada”: Uma Promise criada mas não manipulada (sem .then() ou .catch()) pode resultar em operações sendo executadas sem que seus resultados ou erros sejam observados, levando a comportamentos inesperados (embora geralmente o ambiente Node.js avise sobre Promises rejeitadas sem tratamento).

Implementação Prática

Vamos desenvolver um exemplo prático que simula uma operação assíncrona, como a leitura de um arquivo, utilizando Promises. Este código pode ser executado diretamente em um ambiente Node.js. Para simular a complexidade de um ambiente real, incluiremos um atraso e a possibilidade de erro.

Primeiro, crie um arquivo chamado app.js.

// app.js

// Importando o módulo 'fs/promises' para operações de arquivo assíncronas que retornam Promises. // Nota: Em cenários mais simples ou legados, o 'fs' puro com callbacks era comum, // mas 'fs/promises' é a abordagem moderna e recomendada para trabalhar com Promises. const fs = require('fs/promises');

// Importando o módulo 'path' para lidar com caminhos de arquivos de forma segura e multiplataforma. const path = require('path');

// Definindo um caminho para um arquivo de exemplo. // O arquivo será 'dados.txt' na mesma pasta do 'app.js'. const caminhoArquivo = path.join(__dirname, 'dados.txt');

// --- Função para simular uma operação assíncrona com Promise --- /* Simula a leitura de um arquivo. Esta função retorna uma Promise que será resolvida com o conteúdo do arquivo ou rejeitada em caso de erro. @param {string} caminho O caminho completo do arquivo a ser lido. @returns {Promise} Uma Promise que resolve com o conteúdo do arquivo. / function lerArquivoAssincrono(caminho) { // Retornamos uma nova Promise. O construtor da Promise recebe uma função // com dois argumentos: resolve (para sucesso) e reject (para falha). return new Promise(async (resolve, reject) => { // Simulamos um atraso artificial para mimetizar uma operação de I/O real, // que naturalmente levaria tempo. Aqui, 2 segundos (2000 milissegundos). console.log([${new Date().toLocaleTimeString()}] Tentando ler o arquivo: ${caminho}); await new Promise(res => setTimeout(res, 2000)); // Espera 2 segundos

try { // Tentamos ler o arquivo de forma assíncrona usando 'fs/promises'. // O '.readFile' já retorna uma Promise, então podemos usar await aqui dentro // de um bloco async para um código mais linear. const conteudo = await fs.readFile(caminho, { encoding: 'utf8' }); console.log([${new Date().toLocaleTimeString()}] Arquivo '${path.basename(caminho)}' lido com sucesso.); // Se a leitura for bem-sucedida, a Promise é resolvida com o conteúdo. resolve(conteudo); } catch (erro) { // Se ocorrer qualquer erro durante a leitura, a Promise é rejeitada com o erro. console.error([${new Date().toLocaleTimeString()}] Erro ao ler o arquivo '${path.basename(caminho)}':, erro.message); reject(new Error(Falha na leitura do arquivo: ${erro.message})); } }); }

// --- Ponto de Entrada da Aplicação --- async function iniciarAplicacao() { console.log([${new Date().toLocaleTimeString()}] Aplicação iniciada.);

// --- Uso Básico da Promise: .then() e .catch() --- console.log(\n--- Exemplo 1: Leitura de arquivo bem-sucedida ---); lerArquivoAssincrono(caminhoArquivo) // O método .then() é executado quando a Promise é resolvida (sucesso). // Ele recebe o valor que foi passado para 'resolve()'. .then(conteudo => { console.log([${new Date().toLocaleTimeString()}] Resultado do Exemplo 1 (Sucesso):); console.log(Conteúdo do arquivo: "${conteudo.trim()}"); }) // O método .catch() é executado quando a Promise é rejeitada (erro). // Ele recebe o valor que foi passado para 'reject()'. .catch(erro => { console.error([${new Date().toLocaleTimeString()}] Resultado do Exemplo 1 (Erro):, erro.message); }) // O método .finally() é sempre executado, independentemente de a Promise // ter sido resolvida ou rejeitada. Útil para limpeza de recursos. .finally(() => { console.log([${new Date().toLocaleTimeString()}] Exemplo 1 finalizado (operação de limpeza, etc.).); });

// --- Exemplo 2: Tratamento de erro (arquivo inexistente) --- console.log(\n--- Exemplo 2: Leitura de arquivo inexistente ---); const arquivoInexistente = path.join(__dirname, 'arquivo_nao_existe.txt'); lerArquivoAssincrono(arquivoInexistente) .then(conteudo => { console.log([${new Date().toLocaleTimeString()}] Resultado do Exemplo 2 (Sucesso - Não deveria ocorrer):, conteudo); }) .catch(erro => { // Este .catch() irá capturar o erro de 'arquivo_nao_existe.txt'. console.error([${new Date().toLocaleTimeString()}] Resultado do Exemplo 2 (Erro esperado):, erro.message); }) .finally(() => { console.log([${new Date().toLocaleTimeString()}] Exemplo 2 finalizado (operação de limpeza, etc.).); });

// --- Exemplo 3: Encadeamento de Promises --- // Múltiplas operações assíncronas em sequência. // O resultado de uma Promise pode ser passado como entrada para a próxima. console.log(\n--- Exemplo 3: Encadeamento de Promises ---); lerArquivoAssincrono(caminhoArquivo) .then(conteudo => { console.log([${new Date().toLocaleTimeString()}] Etapa 1: Arquivo lido. Conteúdo: "${conteudo.trim().substring(0, 15)}..."); // Aqui, simulamos uma segunda operação assíncrona que processa o conteúdo. // É crucial RETORNAR uma nova Promise para que o próximo .then() possa encadear. return new Promise(res => { setTimeout(() => { const conteudoProcessado = conteudo.toUpperCase(); console.log([${new Date().toLocaleTimeString()}] Etapa 2: Conteúdo processado para maiúsculas.); res(conteudoProcessado); }, 1500); // Mais 1.5 segundos de atraso }); }) .then(conteudoFinal => { // Este .then() recebe o resultado da Promise retornada na etapa anterior. console.log([${new Date().toLocaleTimeString()}] Etapa 3: Conteúdo final após processamento: "${conteudoFinal.trim().substring(0, 30)}..."); // Podemos retornar mais uma Promise, se necessário, ou um valor direto. return Processamento completo e finalizado.; }) .catch(erro => { console.error([${new Date().toLocaleTimeString()}] Erro no encadeamento de Promises:, erro.message); }) .finally(() => { console.log([${new Date().toLocaleTimeString()}] Exemplo 3 finalizado.); });

console.log([${new Date().toLocaleTimeString()}] Operações assíncronas disparadas. A aplicação continua executando outras tarefas.); // O código aqui é executado imediatamente, sem esperar as Promises acima terminarem, // demonstrando a natureza não-bloqueante. }

// Cria um arquivo de exemplo para ser lido pela Promise. // Use fs.writeFile para garantir que o arquivo exista antes de tentar ler. fs.writeFile(caminhoArquivo, 'Este e um exemplo de conteudo do arquivo dados.txt.\nLinha 2 de teste.', { encoding: 'utf8' }) .then(() => { console.log([${new Date().toLocaleTimeString()}] Arquivo de exemplo '${path.basename(caminhoArquivo)}' criado com sucesso.); iniciarAplicacao(); // Inicia a aplicação após criar o arquivo. }) .catch(err => { console.error([${new Date().toLocaleTimeString()}] Erro ao criar arquivo de exemplo:, err.message); // Se não conseguir criar o arquivo, ainda tentamos iniciar a aplicação, // mas o Exemplo 1 falhará, demonstrando o .catch(). iniciarAplicacao(); });

Para rodar este código, você precisa criar um arquivo dados.txt na mesma pasta ou deixar que o próprio script o crie, como está configurado. O script cria o arquivo e então chama iniciarAplicacao(). Caso o dados.txt não seja criado, o primeiro exemplo de Promise também cairá no .catch(), o que é ótimo para demonstrar o tratamento de erros!

Salve o código acima como app.js e execute-o via terminal:

node app.js

Melhores Práticas Enterprise

    • Retorne Promises: Sempre que uma função realizar uma operação assíncrona, faça-a retornar uma Promise. Isso facilita o encadeamento e o tratamento de erros.
    • Tratamento de Erros Explícito: Utilize .catch() em todas as cadeias de Promises para viabilizar a captura e o log de erros. Uma Promise rejeitada sem um .catch() pode levar a um erro não tratado que derruba sua aplicação Node.js.
    • Imutabilidade: Evite modificar variáveis globais ou externas dentro dos .then()/.catch(). Prefira passar os dados como resultado das Promises.
    • Sintaxe async/await: Para projetos modernos, async/await é a forma preferencial de trabalhar com Promises, tornando o código ainda mais próximo do síncrono e mais legível. Vamos explorar isso em aulas futuras.
    • Logging Profissional: Utilize um bom sistema de logging (como winston ou pino) em vez de console.log para ambientes de produção. Nossos exemplos usam console.log para clareza didática.

Configurações Específicas para HostGator Plano M

As Promises são uma característica fundamental do JavaScript e do ambiente Node.js. Seu funcionamento é universal e independente do provedor de hospedagem. No HostGator Plano M, onde você geralmente tem acesso a um ambiente Node.js via SSH e pode gerenciar seus arquivos e processos, o código que utiliza Promises funcionará exatamente como em sua máquina local ou em qualquer outro servidor.

A única consideração seria garantir que o Node.js esteja instalado na versão compatível com seu código e que o ambiente tenha os recursos (CPU, memória) para executar suas operações assíncronas. O código fornecido aqui é 100% compatível e rodará sem problemas, desde que o ambiente Node.js esteja configurado.

Testes Básicos Incluídos

Os console.log no nosso exemplo servem como “testes” básicos, mostrando o fluxo de execução, os resultados de sucesso e os cenários de erro. Em um ambiente enterprise, você utilizaria frameworks de teste (como Jest ou Mocha) para escrever testes unitários e de integração que validam o comportamento das suas funções baseadas em Promises.

Exercício Hands-On

Agora é a sua vez de aplicar o que aprendemos!

Desafio Prático

Crie uma nova função chamada simularConexaoBancoDeDados que:

    • Receba um parâmetro idUsuario.
    • Retorne uma Promise.
    • Dentro da Promise, simule um atraso de 3 segundos.
    • Após o atraso, se o idUsuario for um número par, a Promise deve ser resolvida com uma mensagem indicando que o “Usuário [idUsuario] encontrado e dados carregados”.
    • Se o idUsuario for um número ímpar, a Promise deve ser rejeitada com uma mensagem de erro “Usuário [idUsuario] não encontrado no banco de dados.”.
    • Chame esta função com um idUsuario par e com um idUsuario ímpar, utilizando .then() e .catch() para logar os resultados.

Solução Detalhada Passo a Passo

// Adicione esta seção ao seu arquivo app.js ou crie um novo.

// --- Nova Função para o Exercício --- / Simula uma conexão com banco de dados para buscar um usuário. Resolve se o idUsuario for par, rejeita se for ímpar. @param {number} idUsuario O ID do usuário a ser buscado. @returns {Promise} Uma Promise que resolve com a mensagem de sucesso ou rejeita com a mensagem de erro. */ function simularConexaoBancoDeDados(idUsuario) { return new Promise((resolve, reject) => { console.log([${new Date().toLocaleTimeString()}] Tentando conectar ao banco para idUsuario: ${idUsuario}); // Simula um atraso de 3 segundos para a 'conexão' e 'consulta'. setTimeout(() => { if (idUsuario % 2 === 0) { // Se o ID for par, resolvemos a Promise. const mensagemSucesso = Usuário ${idUsuario} encontrado e dados carregados com sucesso.; console.log([${new Date().toLocaleTimeString()}] Sucesso DB para ${idUsuario}.); resolve(mensagemSucesso); } else { // Se o ID for ímpar, rejeitamos a Promise. const mensagemErro = Usuário ${idUsuario} não encontrado no banco de dados.; console.error([${new Date().toLocaleTimeString()}] Erro DB para ${idUsuario}.); reject(new Error(mensagemErro)); } }, 3000); // Atraso de 3 segundos }); }

// --- Chamadas para a nova função --- async function executarExerciciosPromises() { console.log(\n--- Exercício: Simulação de Conexão com Banco de Dados ---);

// Chamada com ID par (sucesso esperado) console.log(\nDisparando consulta para ID par (10).); simularConexaoBancoDeDados(10) .then(resultado => { console.log([${new Date().toLocaleTimeString()}] RESULTADO SUCESSO EXERCÍCIO:, resultado); }) .catch(erro => { console.error([${new Date().toLocaleTimeString()}] RESULTADO ERRO EXERCÍCIO (não deveria ocorrer para ID par):, erro.message); }) .finally(() => { console.log([${new Date().toLocaleTimeString()}] Consulta para ID 10 finalizada.); });

// Chamada com ID ímpar (erro esperado) console.log(\nDisparando consulta para ID ímpar (7).); simularConexaoBancoDeDados(7) .then(resultado => { console.log([${new Date().toLocaleTimeString()}] RESULTADO SUCESSO EXERCÍCIO (não deveria ocorrer para ID ímpar):, resultado); }) .catch(erro => { console.error([${new Date().toLocaleTimeString()}] RESULTADO ERRO EXERCÍCIO (esperado para ID ímpar):, erro.message); }) .finally(() => { console.log([${new Date().toLocaleTimeString()}] Consulta para ID 7 finalizada.); });

console.log([${new Date().toLocaleTimeString()}] Ambas as consultas de banco de dados foram disparadas, aguardando resultados...); }

// Chame a função do exercício após as outras operações ou de forma isolada. // Por exemplo, você pode adicionar 'executarExerciciosPromises();' no final do 'iniciarAplicacao()' // ou criar um novo arquivo para este exercício. // Para este exemplo, vou chamar isoladamente para evitar sobrecarga no log anterior. // Certifique-se de comentar as chamadas anteriores se estiver no mesmo arquivo para evitar confusão no log. // executarExerciciosPromises();

Para executar o exercício, você pode adicionar a chamada executarExerciciosPromises(); no final do seu arquivo app.js (comentando as chamadas anteriores para evitar um log muito longo) ou criar um novo arquivo apenas para o exercício e executá-lo.

Como Testar e Validar o Resultado

Execute o script Node.js novamente. Observe a saída no terminal:

    • Você verá as mensagens de “Tentando conectar ao banco…” para ambos os IDs.
    • Após 3 segundos (e independentemente das outras Promises da Implementação Prática, que rodam em paralelo), a mensagem de sucesso para o ID par (10) deve aparecer.
    • Quase simultaneamente (ou em uma ordem um pouco diferente dependendo da execução), a mensagem de erro para o ID ímpar (7) deve surgir.
    • As mensagens “Consulta para ID X finalizada” aparecerão logo em seguida.

Isso valida que sua Promise está sendo resolvida e rejeitada corretamente, e que os métodos .then() e .catch() estão capturando os resultados esperados.

Troubleshooting dos Erros Mais Comuns

    • Promise { } na saída: Você pode ter esquecido de usar .then() e .catch() na sua Promise. Se você apenas chamar simularConexaoBancoDeDados(10) sem os manipuladores, o Node.js mostrará o objeto Promise em seu estado pendente.
    • Erro “UnhandledPromiseRejectionWarning”: Isso ocorre se sua Promise for rejeitada, mas não houver um .catch() para lidar com o erro. Certifique-se de sempre adicionar um .catch().
    • Código não executa: Verifique se a função simularConexaoBancoDeDados está sendo chamada e se você adicionou o setTimeout corretamente.
    • Resultados não aparecem em sequência: Lembre-se que as Promises são assíncronas! Elas são disparadas e o restante do seu código continua a executar. Os resultados aparecerão quando as Promises forem resolvidas ou rejeitadas, não necessariamente na ordem que foram escritas no código de chamada.

Próximos Passos Sugeridos

Para aprofundar ainda mais seu conhecimento, explore:

    • Promise.all(): Para executar múltiplas Promises em paralelo e esperar que todas sejam concluídas. Ideal para carregar múltiplos recursos independentes.
    • Promise.race(): Para executar múltiplas Promises e agir com base na primeira que for resolvida ou rejeitada.
    • async/await: A sintaxe mais moderna e legível para trabalhar com Promises, tornando o código assíncrono quase tão fácil de ler quanto o síncrono. Esta será a base da nossa próxima aula!

Parabéns por completar mais esta etapa essencial na sua formação como especialista em APIs! As Promises são um conceito poderoso que o habilita a criar aplicações robustas e performáticas.

🚀 Pronto para a próxima aula?

Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!

📚 Ver todas as aulas