Promises vs Async/Await: evitando o inferno de callbacks moderno

1. O Inferno de Callbacks: Origem do Problema

1.1. O padrão clássico de callbacks em JavaScript e Node.js

Antes das Promises e do async/await, o JavaScript assíncrono era dominado por callbacks — funções passadas como argumento para serem executadas após a conclusão de uma operação. Esse padrão era onipresente no Node.js, especialmente em operações de I/O, leitura de arquivos e requisições HTTP.

// Exemplo clássico de callback em Node.js
const fs = require('fs');

fs.readFile('arquivo.txt', 'utf8', (erro, dados) => {
  if (erro) {
    console.error('Erro ao ler arquivo:', erro);
    return;
  }
  console.log('Conteúdo:', dados);
});

1.2. Aninhamento excessivo e perda de legibilidade (callback hell)

O problema se agravava quando múltiplas operações assíncronas dependiam umas das outras, criando estruturas profundamente aninhadas conhecidas como "callback hell" ou "pirâmide da desgraça".

// Callback hell: múltiplas operações aninhadas
fs.readFile('usuario.json', 'utf8', (erro, usuario) => {
  if (erro) {
    console.error('Erro:', erro);
    return;
  }
  const usuarioId = JSON.parse(usuario).id;

  db.buscarPedidos(usuarioId, (erro, pedidos) => {
    if (erro) {
      console.error('Erro ao buscar pedidos:', erro);
      return;
    }

    api.enviarEmail(pedidos[0].email, (erro, resultado) => {
      if (erro) {
        console.error('Erro ao enviar email:', erro);
        return;
      }

      console.log('Email enviado:', resultado);
    });
  });
});

1.3. Dificuldades no tratamento de erros e fluxos condicionais

Cada callback exigia verificação manual de erro, levando a código repetitivo e propenso a falhas. Fluxos condicionais tornavam-se quase impossíveis de gerenciar de forma limpa.

2. Promises: A Primeira Onda de Organização

2.1. Conceitos fundamentais: estados (pending, fulfilled, rejected) e encadeamento

Promises introduziram um objeto que representa a eventual conclusão (ou falha) de uma operação assíncrona, com três estados possíveis: pending (pendente), fulfilled (resolvida) e rejected (rejeitada).

// Criando uma Promise simples
const promessa = new Promise((resolve, reject) => {
  setTimeout(() => {
    const sucesso = true;
    if (sucesso) {
      resolve('Operação concluída com sucesso!');
    } else {
      reject('Algo deu errado.');
    }
  }, 1000);
});

2.2. Métodos .then(), .catch() e .finally() na prática

O encadeamento de Promises permitiu escrever código assíncrono de forma mais linear:

// Encadeamento de Promises
buscarUsuario(1)
  .then(usuario => buscarPedidos(usuario.id))
  .then(pedidos => enviarEmail(pedidos[0].email))
  .then(resultado => console.log('Email enviado:', resultado))
  .catch(erro => console.error('Erro:', erro))
  .finally(() => console.log('Operação finalizada'));

2.3. Limitações ainda presentes: verbosidade e encadeamento profundo

Apesar da melhora, Promises ainda podiam gerar encadeamentos longos e difíceis de ler, especialmente com múltiplos níveis de dependência e tratamento de erros específicos.

3. Async/Await: Sintaxe Síncrona para Código Assíncrono

3.1. A palavra-chave async e o retorno implícito de Promises

A declaração async transforma qualquer função em uma função que retorna uma Promise, permitindo o uso de await internamente.

// Função async que retorna uma Promise implicitamente
async function obterDados() {
  return 'Dados processados';
}

obterDados().then(console.log); // 'Dados processados'

3.2. await: pausando a execução sem bloquear a thread

await pausa a execução da função async até que a Promise seja resolvida, sem bloquear a thread principal:

async function processarUsuario(id) {
  const usuario = await buscarUsuario(id);
  const pedidos = await buscarPedidos(usuario.id);
  const email = await enviarEmail(pedidos[0].email);
  return email;
}

3.3. Comparação visual: mesmo fluxo com Promises vs. async/await

// Com Promises
function obterDadosUsuario(id) {
  return buscarUsuario(id)
    .then(usuario => buscarPerfil(usuario.perfilId))
    .then(perfil => formatarResposta(usuario, perfil));
}

// Com async/await
async function obterDadosUsuario(id) {
  const usuario = await buscarUsuario(id);
  const perfil = await buscarPerfil(usuario.perfilId);
  return formatarResposta(usuario, perfil);
}

4. Tratamento de Erros em Cada Abordagem

4.1. Erros em Promises: catch único vs. múltiplos catch encadeados

// Promise com catch único
buscarDados()
  .then(processar)
  .catch(erro => console.error('Erro geral:', erro));

// Promise com catches específicos
buscarDados()
  .then(dados => processar(dados))
  .catch(erro => {
    if (erro instanceof NetworkError) {
      return tratarErroRede(erro);
    }
    throw erro;
  })
  .catch(erro => console.error('Erro não tratado:', erro));

4.2. Erros em async/await: try/catch tradicional e sua clareza

async function processarDados() {
  try {
    const dados = await buscarDados();
    const resultado = await processar(dados);
    return resultado;
  } catch (erro) {
    if (erro instanceof NetworkError) {
      return tratarErroRede(erro);
    }
    console.error('Erro não tratado:', erro);
    throw erro;
  }
}

4.3. Erros não capturados e rejeições silenciosas: como evitar

Sempre use catch em Promises ou try/catch em async/await. Para rejeições não capturadas, utilize process.on('unhandledRejection') em Node.js.

5. Execução Paralela e Sequencial

5.1. Promises com Promise.all() e Promise.race() para concorrência

// Execução paralela com Promise.all
Promise.all([
  buscarUsuario(1),
  buscarUsuario(2),
  buscarUsuario(3)
]).then(([usuario1, usuario2, usuario3]) => {
  console.log('Todos os usuários:', usuario1, usuario2, usuario3);
});

// Promise.race: a primeira Promise resolvida vence
Promise.race([
  fazerRequisicao('servidor1.com'),
  fazerRequisicao('servidor2.com')
]).then(resposta => console.log('Resposta mais rápida:', resposta));

5.2. Async/await com Promise.all(): o melhor dos dois mundos

async function buscarTodosUsuarios(ids) {
  const promessas = ids.map(id => buscarUsuario(id));
  const usuarios = await Promise.all(promessas);
  return usuarios;
}

5.3. Loops assíncronos: for...of com await vs. Promise.all() em arrays

// Sequencial (lento)
async function processarSequencial(itens) {
  for (const item of itens) {
    await processarItem(item);
  }
}

// Paralelo (rápido)
async function processarParalelo(itens) {
  await Promise.all(itens.map(item => processarItem(item)));
}

6. Casos de Uso Avançados e Boas Práticas

6.1. Timeouts e cancelamento de Promises (AbortController)

async function buscarComTimeout(url, tempoLimite = 5000) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), tempoLimite);

  try {
    const resposta = await fetch(url, { signal: controller.signal });
    return await resposta.json();
  } finally {
    clearTimeout(timeout);
  }
}

6.2. Transformação de APIs baseadas em callbacks em Promises (promisify)

const { promisify } = require('util');
const fs = require('fs');

const readFileAsync = promisify(fs.readFile);

async function lerArquivo(caminho) {
  const conteudo = await readFileAsync(caminho, 'utf8');
  return conteudo;
}

6.3. Quando evitar async/await: alta concorrência e performance crítica

Em cenários de alta concorrência com milhares de operações simultâneas, Promises puras podem oferecer melhor performance devido ao menor overhead de contexto.

7. Inferno Moderno: Armadilhas Comuns com Async/Await

7.1. await sequencial desnecessário (serialização acidental)

// Ruim: serialização desnecessária
async function processarRuim(itens) {
  const resultados = [];
  for (const item of itens) {
    const resultado = await processarItem(item); // Aguarda cada um
    resultados.push(resultado);
  }
  return resultados;
}

// Bom: paralelismo real
async function processarBom(itens) {
  const promessas = itens.map(item => processarItem(item));
  return await Promise.all(promessas);
}

7.2. Promessas esquecidas sem await (fire-and-forget problemático)

// Perigoso: Promise não aguardada
async function salvarDados(dados) {
  salvarNoBanco(dados); // Esqueceu o await! Erro pode passar despercebido
  return 'Dados salvos';
}

// Correto
async function salvarDados(dados) {
  await salvarNoBanco(dados);
  return 'Dados salvos';
}

7.3. Misturar estilos no mesmo projeto: inconsistência e bugs sutis

Escolha um padrão e mantenha consistência. Misturar Promises com async/await em um mesmo fluxo pode levar a bugs difíceis de rastrear.

8. Conclusão: Escolhendo a Ferramenta Certa

8.1. Resumo das vantagens e desvantagens de cada abordagem

Promises:
- Vantagens: Excelente para paralelismo (Promise.all, Promise.race), menor overhead em alta concorrência
- Desvantagens: Verbosidade, encadeamento pode ficar confuso

Async/Await:
- Vantagens: Legibilidade superior, tratamento de erros natural com try/catch, fluxo linear
- Desvantagens: Pode induzir serialização acidental, overhead em loops grandes

8.2. Guia prático: quando usar Promises puras vs. async/await

  • Use async/await para fluxos sequenciais, tratamento de erros complexo e quando a legibilidade é prioridade
  • Use Promises puras para paralelismo intenso, operações de alta concorrência e quando você precisa de Promise.allSettled ou Promise.any
  • Combine ambos quando necessário: async/await para o fluxo principal e Promise.all() para paralelismo

8.3. Tendências futuras: top-level await e novas APIs assíncronas

O top-level await (disponível em módulos ES) permite usar await fora de funções async, simplificando ainda mais o código. Novas APIs como Promise.withResolvers() e Promise.try() continuam evoluindo o ecossistema assíncrono.

// Top-level await em módulos ES
const dados = await fetch('https://api.exemplo.com/dados');
console.log(dados);

A escolha entre Promises e async/await não é binária — ambas são ferramentas poderosas no arsenal do desenvolvedor JavaScript moderno. O segredo está em entender quando cada uma brilha e aplicá-las com sabedoria para evitar o inferno de callbacks moderno.

Referências

  • MDN Web Docs: Using Promises — Guia completo sobre Promises em JavaScript, com exemplos práticos e explicações detalhadas dos métodos .then(), .catch() e .finally().

  • MDN Web Docs: async function — Documentação oficial sobre funções async e o operador await, incluindo comportamento e casos de uso.

  • Node.js Documentation: Promises in Node.js — Guia oficial do Node.js sobre Promises, com exemplos de promisify e tratamento de erros em operações assíncronas.

  • JavaScript.info: Async/Await — Tutorial abrangente sobre async/await, com exemplos comparativos entre Promises e async/await, incluindo tratamento de erros e execução paralela.

  • Google Web Dev: Promises vs Async/Await — Artigo técnico do Google sobre as diferenças entre Promises e async/await, com foco em performance e boas práticas para desenvolvimento web.