Fundamentos do Node.js

0/23 aulas0%
pratica

Async/Await: Simplificando o Código Assíncrono

Aprenda sobre async/await: simplificando o código assíncrono

35 min
Aula 4 de 6

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:

  1. 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.
  2. 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() e Promise.catch(), o encadeamento se tornou mais gerenciável.
  3. Async/Await: A cereja do bolo! Construído sobre Promises, async/await permite 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 async sempre retorna uma Promise.
  • Se a função async retornar um valor não-Promise, esse valor será automaticamente envolvido em uma Promise resolvida.
  • Se a função async lanç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 async até 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 await lançará uma exceção, que pode ser capturada por um bloco try...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 express

Agora, 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/1
  • GET 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:

  1. Crie uma nova função executarFluxoAsyncAwait() que refatore o código acima usando async/await.
  2. Garanta que o tratamento de erros seja feito com try...catch.
  3. Execute a função para ambos os cenários (sucesso e falha).

Checklist:

  • Crie a função executarFluxoAsyncAwait.
  • Use await para buscarDadosAPI.
  • Use await para processarDados.
  • Implemente try...catch para 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.json inicial (pode ser um array vazio []).
  • Implemente a rota GET /tasks usando async/await e readFile.
  • Implemente a rota GET /tasks/:id usando async/await e readFile.
  • Implemente a rota POST /tasks usando async/await, readFile, writeFile e gere um ID único (ex: Date.now().toString()).
  • Implemente a rota PUT /tasks/:id usando async/await, readFile, writeFile.
  • Implemente a rota DELETE /tasks/:id usando async/await, readFile, writeFile.
  • Use try...catch para 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:

  1. Crie uma função simularAPI(nome, delay, shouldFail = false) que retorna uma Promise que resolve com uma mensagem ou rejeita com um erro após delay milissegundos.
  2. Crie uma função buscarMultiplasAPIs() que chame simularAPI três vezes com diferentes atrasos e nomes.
  3. Use Promise.all dentro de buscarMultiplasAPIs para que as chamadas às APIs sejam executadas em paralelo.
  4. Use await para esperar que Promise.all termine.
  5. Implemente try...catch para lidar com falhas (se uma das APIs falhar, Promise.all rejeita).

Checklist:

  • Crie a função simularAPI.
  • Crie a função buscarMultiplasAPIs e marque-a como async.
  • Dentro de buscarMultiplasAPIs, crie um array de Promises chamando simularAPI vá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 async sempre retornam Promises.
  • await pausa a execução da função async até 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 com async/await.
  • Para operações paralelas, combine async/await com Promise.all.

Próximos Passos:

  • Top-level await: A partir do Node.js 14.8.0, você pode usar await fora de uma função async em módulos ES (arquivos .mjs ou type: "module" no package.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! 👋

© 2025 Escola All Dev. Todos os direitos reservados.

Async/Await: Simplificando o Código Assíncrono - Fundamentos do Node.js | escola.all.dev.br