Fundamentos do Node.js
Async/Await: Simplificando o Código Assíncrono
Aprenda sobre async/await: simplificando o código assíncrono
Async/Await: Simplificando o Código Assíncrono 🚀
Olá, futuros ninjas do Node.js! Sejam bem-vindos a esta aula prática onde vamos desmistificar e dominar uma das funcionalidades mais poderosas e elegantes do JavaScript moderno para lidar com assincronicidade: o async/await.
Chega de callback hell ou de aninhar .then() e .catch() excessivamente! Com async/await, seu código assíncrono parecerá síncrono, tornando-o muito mais legível e fácil de manter.
1. Introdução: A Evolução da Assincronicidade no Node.js 🕰️
No mundo do Node.js, a assincronicidade é o pilar fundamental. Desde o início, lidamos com operações que levam tempo para serem concluídas (como leitura de arquivos, requisições de rede, acesso a banco de dados) sem bloquear o Event Loop.
Historicamente, passamos por algumas fases:
- Callbacks: Funções que são passadas como argumento e executadas quando uma operação assíncrona termina. Embora funcionais, podem levar ao famoso "callback hell" (pirâmide da desgraça) em códigos complexos.
- Promises: Uma evolução que trouxe mais estrutura e legibilidade, representando o eventual resultado (sucesso ou falha) de uma operação assíncrona. Com
Promise.then()ePromise.catch(), o encadeamento se tornou mais gerenciável. - Async/Await: A cereja do bolo! Construído sobre Promises,
async/awaitpermite escrever código assíncrono que se parece e se comporta de forma muito mais próxima ao código síncrono, sem perder os benefícios da não-bloqueabilidade.
Nesta aula, vamos mergulhar fundo no async/await e ver como ele transforma a maneira como escrevemos código assíncrono.
2. Explicação Detalhada: Desvendando async e await ✨
async/await é uma sintaxe açúcar sobre Promises. Isso significa que, por baixo dos panos, ele ainda está trabalhando com Promises, mas de uma forma muito mais intuitiva.
O Keyword async
O async é usado para declarar uma função como assíncrona.
- Uma função
asyncsempre retorna uma Promise. - Se a função
asyncretornar um valor não-Promise, esse valor será automaticamente envolvido em uma Promise resolvida. - Se a função
asynclançar uma exceção, a Promise retornada será rejeitada com essa exceção.
Exemplo básico de função async:
async function minhaFuncaoAssincrona() {
return "Olá, mundo assíncrono!";
}
minhaFuncaoAssincrona().then(mensagem => {
console.log(mensagem); // Saída: Olá, mundo assíncrono!
});
// Ou, se você retornar uma Promise explicitamente:
async function outraFuncaoAssincrona() {
return Promise.resolve("Outra mensagem!");
}
outraFuncaoAssincrona().then(mensagem => {
console.log(mensagem); // Saída: Outra mensagem!
});
// E se houver um erro:
async function funcaoComErro() {
throw new Error("Algo deu errado!");
}
funcaoComErro().catch(erro => {
console.error(erro.message); // Saída: Algo deu errado!
});O Keyword await
O await só pode ser usado dentro de uma função async.
- Ele pausa a execução da função
asyncaté que a Promise à qual ele está aguardando seja resolvida ou rejeitada. - Se a Promise for resolvida, o valor resolvido é retornado pelo
await. - Se a Promise for rejeitada, o
awaitlançará uma exceção, que pode ser capturada por um blocotry...catch.
Importante: await não bloqueia o Event Loop do Node.js. Ele apenas pausa a execução da função async atual, permitindo que outras operações e tarefas assíncronas continuem a ser processadas.
Exemplo de uso de await:
function simularOperacaoAssincrona(ms) {
return new Promise(resolve => setTimeout(() => resolve(`Operação concluída em ${ms}ms`), ms));
}
async function executarOperacoes() {
console.log("Iniciando...");
const resultado1 = await simularOperacaoAssincrona(2000); // Pausa aqui por 2 segundos
console.log(resultado1); // Saída após 2s: Operação concluída em 2000ms
const resultado2 = await simularOperacaoAssincrona(1000); // Pausa aqui por 1 segundo
console.log(resultado2); // Saída após 1s: Operação concluída em 1000ms
console.log("Todas as operações concluídas!");
}
executarOperacoes();Tratamento de Erros com try...catch
Uma das grandes vantagens do async/await é como ele simplifica o tratamento de erros. Em vez de encadear .catch()s, podemos usar o familiar try...catch como faríamos com código síncrono.
function simularFalha(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve(`Sucesso após ${ms}ms`);
} else {
reject(new Error(`Falha após ${ms}ms`));
}
}, ms);
});
}
async function tentarOperacao() {
try {
console.log("Tentando operação...");
const resultado = await simularFalha(1500); // Pode resolver ou rejeitar
console.log("🎉 " + resultado);
} catch (erro) {
console.error("❌ Erro capturado: " + erro.message);
} finally {
console.log("Operação finalizada (com ou sem erro).");
}
}
tentarOperacao();3. Código de Exemplo Oficial: fs/promises 📁
O Node.js modernizou muitos de seus módulos core para oferecer uma API baseada em Promises, perfeita para ser usada com async/await. O módulo fs/promises é um excelente exemplo. Ele fornece versões Promise-based das funções do módulo fs (File System).
Vamos ver como ler um arquivo usando fs/promises com async/await.
Primeiro, crie um arquivo exemplo.txt com o seguinte conteúdo:
Olá do arquivo!
Este é um teste de leitura assíncrona.
Agora, o código Node.js:
// Importa o módulo fs/promises
import { readFile, writeFile } from 'node:fs/promises'; // Usando 'node:' prefixo para módulos core
async function lerEGravarArquivo() {
try {
console.log("Iniciando leitura do arquivo...");
// await readFile retorna o conteúdo do arquivo como um Buffer
const data = await readFile('exemplo.txt', { encoding: 'utf8' });
console.log("Conteúdo do arquivo 'exemplo.txt':");
console.log(data);
const novoConteudo = data + "\n\nConteúdo adicionado via Node.js!";
console.log("\nEscrevendo novo conteúdo em 'saida.txt'...");
await writeFile('saida.txt', novoConteudo);
console.log("Arquivo 'saida.txt' criado com sucesso!");
const saidaLida = await readFile('saida.txt', { encoding: 'utf8' });
console.log("\nConteúdo de 'saida.txt':");
console.log(saidaLida);
} catch (err) {
console.error("❌ Ocorreu um erro durante a operação de arquivo:", err.message);
if (err.code === 'ENOENT') {
console.error("Certifique-se de que 'exemplo.txt' existe no diretório.");
}
}
}
// Chama a função assíncrona
lerEGravarArquivo();Para rodar este código, você precisa salvá-lo como um arquivo .mjs (ex: app.mjs) ou adicionar "type": "module" ao seu package.json para usar import.
// package.json
{
"name": "async-await-fs-example",
"version": "1.0.0",
"description": "Exemplo de async/await com fs/promises",
"main": "app.mjs",
"type": "module", // <--- Adicione esta linha
"scripts": {
"start": "node app.mjs"
},
"keywords": [],
"author": "",
"license": "ISC"
}4. Integração: async/await com Express.js 🌐
async/await brilha especialmente em aplicações web, onde muitas operações são naturalmente assíncronas (requisições a banco de dados, APIs externas, leitura/escrita de arquivos). Vamos ver como integrá-lo com o Express.js.
Primeiro, instale o Express:
npm init -y
npm install expressAgora, o código para um servidor Express que usa async/await para simular uma operação de banco de dados:
import express from 'express';
import { readFile } from 'node:fs/promises'; // Para simular uma leitura de dados
const app = express();
const PORT = 3000;
// Middleware para parsear JSON no corpo da requisição
app.use(express.json());
// Simula uma operação de banco de dados assíncrona
async function buscarDadosUsuario(userId) {
// Em uma aplicação real, isso seria uma query ao DB
return new Promise(resolve => {
setTimeout(() => {
if (userId === '1') {
resolve({ id: '1', nome: 'Alice', email: 'alice@example.com' });
} else if (userId === '2') {
resolve({ id: '2', nome: 'Bob', email: 'bob@example.com' });
} else {
resolve(null); // Usuário não encontrado
}
}, 1000); // Simula um atraso de 1 segundo
});
}
// Rota para buscar um usuário por ID
app.get('/usuarios/:id', async (req, res) => {
const { id } = req.params;
try {
const usuario = await buscarDadosUsuario(id); // Aguarda o "DB"
if (usuario) {
res.json(usuario);
} else {
res.status(404).json({ message: `Usuário com ID ${id} não encontrado.` });
}
} catch (error) {
console.error("Erro ao buscar usuário:", error);
res.status(500).json({ message: "Erro interno do servidor." });
}
});
// Rota para ler um arquivo de configuração (simulando um serviço)
app.get('/config', async (req, res) => {
try {
// Em uma aplicação real, você pode ter um arquivo de configuração JSON
// ou buscar de um serviço de configuração externo.
const configData = await readFile('config.json', { encoding: 'utf8' });
const config = JSON.parse(configData);
res.json(config);
} catch (error) {
if (error.code === 'ENOENT') {
res.status(404).json({ message: "Arquivo de configuração 'config.json' não encontrado." });
} else {
console.error("Erro ao ler configuração:", error);
res.status(500).json({ message: "Erro interno do servidor ao carregar configuração." });
}
}
});
// Crie um arquivo config.json na mesma pasta:
// { "api_version": "1.0", "environment": "development" }
// Middleware de tratamento de erros genérico (captura erros não tratados em rotas async)
// É crucial ter um middleware de erro para rotas async/await no Express
app.use((err, req, res, next) => {
console.error("Erro não tratado:", err.stack);
res.status(500).send('Algo deu errado no servidor!');
});
app.listen(PORT, () => {
console.log(`Servidor rodando em http://localhost:${PORT} 🚀`);
console.log(`Testar: http://localhost:${PORT}/usuarios/1`);
console.log(`Testar: http://localhost:${PORT}/config`);
});Crie um arquivo config.json na mesma pasta do seu app.mjs (ou .js se não usar "type": "module"):
// config.json
{
"api_version": "1.0",
"environment": "development",
"database_url": "mongodb://localhost:27017/mydatabase"
}Para rodar: node seu-arquivo.mjs (ou node seu-arquivo.js).
Agora você pode testar as rotas:
GET http://localhost:3000/usuarios/1GET http://localhost:3000/usuarios/3(para ver o 404)GET http://localhost:3000/config
5. Exercícios Práticos: Mão na Massa! 💪
Chegou a hora de aplicar o que aprendemos. Complete os desafios abaixo.
Desafio 1: Refatorando com async/await
Você tem um código que usa Promises encadeadas. Sua tarefa é refatorá-lo para usar async/await, tornando-o mais legível.
Código Original (Promise-based):
function buscarDadosAPI(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url.includes("success")) {
resolve({ data: `Dados de ${url}` });
} else {
reject(new Error(`Falha ao buscar dados de ${url}`));
}
}, 500);
});
}
function processarDados(dados) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ processedData: `Processado: ${dados.data}` });
}, 300);
});
}
buscarDadosAPI("https://api.example.com/success")
.then(dadosBrutos => {
console.log("Dados brutos recebidos:", dadosBrutos);
return processarDados(dadosBrutos);
})
.then(dadosProcessados => {
console.log("Dados processados:", dadosProcessados);
})
.catch(error => {
console.error("Erro na sequência:", error.message);
});
buscarDadosAPI("https://api.example.com/fail")
.then(dadosBrutos => {
console.log("Dados brutos recebidos:", dadosBrutos);
return processarDados(dadosBrutos);
})
.then(dadosProcessados => {
console.log("Dados processados:", dadosProcessados);
})
.catch(error => {
console.error("Erro na sequência (falha):", error.message);
});Sua Tarefa:
- Crie uma nova função
executarFluxoAsyncAwait()que refatore o código acima usandoasync/await. - Garanta que o tratamento de erros seja feito com
try...catch. - Execute a função para ambos os cenários (sucesso e falha).
Checklist:
- Crie a função
executarFluxoAsyncAwait. - Use
awaitparabuscarDadosAPI. - Use
awaitparaprocessarDados. - Implemente
try...catchpara lidar com erros. - Chame a função para testar sucesso e falha.
// Solução para o Desafio 1 (não mostrar inicialmente, apenas para referência do professor)
/*
async function executarFluxoAsyncAwait(url) {
try {
console.log(`\n--- Executando fluxo para ${url} ---`);
const dadosBrutos = await buscarDadosAPI(url);
console.log("Dados brutos recebidos (async/await):", dadosBrutos);
const dadosProcessados = await processarDados(dadosBrutos);
console.log("Dados processados (async/await):", dadosProcessados);
} catch (error) {
console.error("Erro na sequência (async/await):", error.message);
}
}
executarFluxoAsyncAwait("https://api.example.com/success");
executarFluxoAsyncAwait("https://api.example.com/fail");
*/Desafio 2: Construindo um Simples Gerenciador de Tarefas (API REST)
Crie uma pequena API REST usando Express e async/await para gerenciar tarefas em um arquivo JSON.
Funcionalidades:
GET /tasks: Retorna todas as tarefas.GET /tasks/:id: Retorna uma tarefa específica.POST /tasks: Adiciona uma nova tarefa.PUT /tasks/:id: Atualiza uma tarefa existente.DELETE /tasks/:id: Remove uma tarefa.
As tarefas serão armazenadas em um arquivo tasks.json. Use fs/promises para ler e escrever no arquivo.
Estrutura da Tarefa:
{
"id": "UUID_OU_NUMERO_INCREMENTAL",
"title": "Comprar pão",
"completed": false
}Checklist:
- Inicialize um projeto Node.js com Express.
- Crie um arquivo
tasks.jsoninicial (pode ser um array vazio[]). - Implemente a rota
GET /tasksusandoasync/awaitereadFile. - Implemente a rota
GET /tasks/:idusandoasync/awaitereadFile. - Implemente a rota
POST /tasksusandoasync/await,readFile,writeFilee gere um ID único (ex:Date.now().toString()). - Implemente a rota
PUT /tasks/:idusandoasync/await,readFile,writeFile. - Implemente a rota
DELETE /tasks/:idusandoasync/await,readFile,writeFile. - Use
try...catchpara todas as operações de arquivo e API. - Adicione um middleware de erro genérico no Express.
- Teste todas as rotas com uma ferramenta como Postman ou
curl.
Desafio 3: Execução Concorrente com Promise.all e async/await
Quando você tem várias operações assíncronas independentes que precisam ser executadas, await sequencialmente pode ser ineficiente. Promise.all permite executá-las em paralelo. Combine async/await com Promise.all.
Sua Tarefa:
- Crie uma função
simularAPI(nome, delay, shouldFail = false)que retorna uma Promise que resolve com uma mensagem ou rejeita com um erro apósdelaymilissegundos. - Crie uma função
buscarMultiplasAPIs()que chamesimularAPItrês vezes com diferentes atrasos e nomes. - Use
Promise.alldentro debuscarMultiplasAPIspara que as chamadas às APIs sejam executadas em paralelo. - Use
awaitpara esperar quePromise.alltermine. - Implemente
try...catchpara lidar com falhas (se uma das APIs falhar,Promise.allrejeita).
Checklist:
- Crie a função
simularAPI. - Crie a função
buscarMultiplasAPIse marque-a comoasync. - Dentro de
buscarMultiplasAPIs, crie um array de Promises chamandosimularAPIvárias vezes. - Use
await Promise.all(arrayOfPromises). - Imprima os resultados ou o erro.
- Teste cenários onde todas as chamadas são bem-sucedidas e onde uma delas falha.
// Solução para o Desafio 3 (não mostrar inicialmente, apenas para referência do professor)
/*
function simularAPI(nome, delay, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error(`Falha na API ${nome}`));
} else {
resolve(`Dados da API ${nome} (após ${delay}ms)`);
}
}, delay);
});
}
async function buscarMultiplasAPIs() {
try {
console.log("\n--- Buscando múltiplas APIs em paralelo ---");
const promises = [
simularAPI("Serviço A", 1500),
simularAPI("Serviço B", 800),
simularAPI("Serviço C", 2000, false) // Mude para true para testar falha
];
const resultados = await Promise.all(promises);
console.log("Todos os serviços responderam:");
resultados.forEach(res => console.log(`- ${res}`));
} catch (error) {
console.error("❌ Um dos serviços falhou:", error.message);
} finally {
console.log("Busca paralela finalizada.");
}
}
buscarMultiplasAPIs();
*/6. Resumo e Próximos Passos 🏁
Parabéns! Você dominou o async/await!
Em resumo:
async/awaité a forma mais moderna e legível de lidar com código assíncrono em JavaScript/Node.js.- Funções
asyncsempre retornam Promises. awaitpausa a execução da funçãoasyncaté que uma Promise seja resolvida, sem bloquear o Event Loop.- O tratamento de erros é simplificado com
try...catch. - Módulos core do Node.js (como
fs/promises) e frameworks (como Express) se integram perfeitamente comasync/await. - Para operações paralelas, combine
async/awaitcomPromise.all.
Próximos Passos:
- Top-level
await: A partir do Node.js 14.8.0, você pode usarawaitfora de uma funçãoasyncem módulos ES (arquivos.mjsoutype: "module"nopackage.json). Explore essa funcionalidade! - Streams: Para lidar com grandes volumes de dados de forma eficiente, especialmente com arquivos ou rede, o Node.js usa Streams. Muitos Streams podem ser consumidos de forma assíncrona usando
for await...of. - Event Emitters: Outro padrão assíncrono fundamental no Node.js para lidar com eventos.
- Continue praticando! A melhor forma de solidificar o conhecimento é aplicando-o em projetos reais.
Continue codificando e explorando o vasto mundo do Node.js! Até a próxima! 👋