O JavaScript opera em uma única thread, o que significa que ele executa uma instrução por vez. No entanto, para criar aplicações web modernas e fluidas, precisamos lidar com operações demoradas — como requisições de API ou leitura de arquivos — sem travar a interface do usuário. É aqui que entra a programação assíncrona. Entender a evolução dos Callbacks para o Async/Await é essencial para qualquer desenvolvedor que busca escrever código limpo e performático.

A Evolução: Da Confusão à Clareza

A Era dos Callbacks

Antigamente, usávamos funções de retorno (callbacks) para gerenciar o assincronismo. O problema surgia quando precisávamos de múltiplas operações dependentes, resultando no infame "Callback Hell" ou "Pirâmide do Destino".

Exemplo de Callback Hell
// O código torna-se difícil de ler e manter rapidamente
buscarUsuario(id, (err, usuario) => {
  if (err) return tratarErro(err);
  buscarPedidos(usuario.id, (err, pedidos) => {
    if (err) return tratarErro(err);
    detalharPedido(pedidos[0].id, (err, detalhes) => {
      if (err) return tratarErro(err);
      console.log(detalhes); // Sucesso, mas a que custo?
    });
  });
});

Promises: Uma Promessa de Organização

As Promises introduziram uma forma mais elegante de encadear operações. Em vez de passar funções, passamos a lidar com objetos que representam o sucesso ou a falha de uma operação futura.

Cadeia de Promises
buscarUsuario(id)
  .then(usuario => buscarPedidos(usuario.id))
  .then(pedidos => detalharPedido(pedidos[0].id))
  .then(detalhes => console.log(detalhes))
  .catch(err => tratarErro(err))
  .finally(() => removerLoading());

Async/Await: O Açúcar Sintático Perfeito

Introduzido no ES2017, o async/await permite escrever código assíncrono que se parece com código síncrono, facilitando imensamente a leitura e o tratamento de exceções com blocos try/catch.

Sintaxe Async/Await
async function obterDadosCompletos(id) {
  try {
    const usuario = await buscarUsuario(id);
    const pedidos = await buscarPedidos(usuario.id);
    const detalhes = await detalharPedido(pedidos[0].id);
    return detalhes;
  } catch (err) {
    tratarErro(err);
  } finally {
    removerLoading();
  }
}

Criando e Gerenciando Suas Próprias Promises

Às vezes, você precisa transformar uma lógica baseada em tempo ou um evento antigo em uma Promise. Para isso, usamos o construtor new Promise.

Construtor de Promise com Timeout
function aguardar(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function carregarComLimite(url, tempoLimite) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error("Tempo esgotado")), tempoLimite);

    fetch(url)
      .then(res => {
        clearTimeout(timer);
        resolve(res);
      })
      .catch(err => {
        clearTimeout(timer);
        reject(err);
      });
  });
}

Execução em Paralelo vs Sequencial

Um erro comum é usar await em sequência quando as tarefas não dependem uma da outra, o que torna a aplicação lenta. Para máxima performance, devemos executar tarefas independentes simultaneamente.

Promise.all e Promise.allSettled
// Executa tudo em paralelo - Falha se uma falhar
const [users, products] = await Promise.all([
  fetch("/api/users").then(r => r.json()),
  fetch("/api/products").then(r => r.json())
]);

// Promise.allSettled - Útil quando queremos os resultados mesmo com falhas parciais
const resultados = await Promise.allSettled([
  fetch("/api/config"),
  fetch("/api/analytics") 
]);

resultados.forEach(res => {
  if (res.status === "fulfilled") {
    console.log("Sucesso:", res.value);
  } else {
    console.error("Falha em um dos itens:", res.reason);
  }
});

Tratamento de Erros Moderno

Uma abordagem inspirada na linguagem Go é retornar uma tupla para evitar excesso de blocos try/catch aninhados.

Padrão de Wrapper Assíncrono
// Função utilitária para capturar erros sem crashar
async function capturar(promise) {
  try {
    const data = await promise;
    return [null, data];
  } catch (err) {
    return [err, null];
  }
}

// Uso prático
const [erro, user] = await capturar(buscarUsuario(123));
if (erro) {
  console.log("Erro ao buscar usuário:", erro.message);
} else {
  console.log("Usuário encontrado:", user.nome);
}

Erros Comuns para Evitar

  • Esquecer o await: Se você não usar await, receberá o objeto Promise pendente em vez dos dados reais.
  • Loops Ineficientes: O forEach não funciona bem com async. Prefira o for...of ou Promise.all() com map.
  • Promises não capturadas: Sempre termine suas correntes de Promise com .catch() ou use try/catch para evitar erros globais (unhandled rejections).
  • Bloqueio desnecessário: Não use await em chamadas que podem rodar em segundo plano enquanto você processa outras informações.

Otimize seu Código JavaScript

Utilize nossas ferramentas gratuitas para formatar e comprimir seus scripts antes do deploy.