Javascript

Async/Await: escrevendo código assíncrono de forma limpa Já leu

10 min de leitura

Async/Await: escrevendo código assíncrono de forma limpa
As Promises resolveram o Callback Hell. Mas encadear muitos .then() ainda pode ficar confuso quando há lógica condicional, loops ou mú

As Promises resolveram o Callback Hell. Mas encadear muitos .then() ainda pode ficar confuso quando há lógica condicional, loops ou múltiplas variáveis que precisam ser compartilhadas entre etapas.

O async/await, introduzido no ES2017, é açúcar sintático em cima das Promises — ele não substitui as Promises, apenas fornece uma sintaxe mais limpa para trabalhar com elas. O resultado é código assíncrono que se lê como código síncrono, sem perder nenhum dos benefícios das Promises.


A palavra-chave async

async transforma uma função em uma função assíncrona. Uma função async sempre retorna uma Promise, mesmo que você não use return explicitamente ou retorne um valor simples:

// Função síncrona comum
function somar(a, b) {
  return a + b;
}
console.log(somar(2, 3)); // 5

// Função assíncrona — retorna uma Promise
async function somarAsync(a, b) {
  return a + b;
}

somarAsync(2, 3).then(resultado => console.log(resultado)); // 5

// Também funciona com arrow functions
const multiplicar = async (a, b) => a * b;

A palavra-chave await

await só pode ser usado dentro de funções async. Ele pausa a execução da função até que a Promise seja resolvida — e retorna o valor resolvido:

function esperar(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function exemplo() {
  console.log("Início");
  await esperar(2000); // pausa aqui por 2 segundos
  console.log("2 segundos depois");
  await esperar(1000); // pausa mais 1 segundo
  console.log("Mais 1 segundo depois");
}

exemplo();
console.log("Este executa imediatamente — não espera a função async");

// Saída:
// Início
// Este executa imediatamente — não espera a função async
// (2 segundos)
// 2 segundos depois
// (1 segundo)
// Mais 1 segundo depois

O await pausa somente a função async atual — o restante do programa continua normalmente.


Comparando Promises com async/await

O mesmo código escrito das duas formas:

function buscarUsuario(id) {
  return new Promise(resolve =>
    setTimeout(() => resolve({ id, nome: "Ana", plano: "premium" }), 800)
  );
}

function buscarPedidos(usuarioId) {
  return new Promise(resolve =>
    setTimeout(() => resolve([{ id: 101 }, { id: 102 }]), 600)
  );
}

// ── Com Promises e .then() ──────────────────
function carregarDadosPromise() {
  return buscarUsuario(1)
    .then(usuario => {
      console.log(`Usuário: ${usuario.nome}`);
      return buscarPedidos(usuario.id);
    })
    .then(pedidos => {
      console.log(`Pedidos: ${pedidos.length}`);
    })
    .catch(erro => console.error(erro.message));
}

// ── Com async/await ─────────────────────────
async function carregarDadosAsync() {
  try {
    const usuario = await buscarUsuario(1);
    console.log(`Usuário: ${usuario.nome}`);

    const pedidos = await buscarPedidos(usuario.id);
    console.log(`Pedidos: ${pedidos.length}`);
  } catch (erro) {
    console.error(erro.message);
  }
}

A versão com async/await é mais legível — especialmente quando há lógica condicional entre as etapas.


Tratamento de erros com try/catch

Com async/await, o tratamento de erros volta a usar o familiar try/catch:

async function processarPedido(usuarioId, produtoId) {
  try {
    // Cada await pode lançar um erro se a Promise rejeitar
    const usuario = await buscarUsuario(usuarioId);
    const produto = await buscarProduto(produtoId);

    if (produto.estoque === 0) {
      throw new Error(`Produto "${produto.nome}" sem estoque.`);
    }

    const pedido = await criarPedido(usuario.id, produto.id);
    console.log(`✅ Pedido #${pedido.id} criado com sucesso!`);
    return pedido;

  } catch (erro) {
    console.error(`❌ Erro ao processar pedido: ${erro.message}`);
    throw erro; // repropaga se necessário
  } finally {
    console.log("Processo de pedido finalizado.");
  }
}

Lógica condicional — onde async/await brilha

O ponto onde async/await supera claramente o encadeamento de .then():

async function checkout(carrinho, cupom) {
  const usuario = await buscarUsuarioLogado();

  // Lógica condicional — muito mais clara que em .then()
  if (!usuario) {
    throw new Error("Usuário não autenticado.");
  }

  const itensVerificados = await verificarEstoque(carrinho);

  let total = itensVerificados.reduce((acc, item) => acc + item.preco * item.quantidade, 0);

  if (cupom) {
    try {
      const desconto = await validarCupom(cupom, total);
      total = total - desconto;
      console.log(`Cupom aplicado! Desconto: R$ ${desconto}`);
    } catch {
      console.warn("Cupom inválido ou expirado. Continuando sem desconto.");
    }
  }

  const frete = await calcularFrete(usuario.cep);
  total += frete.valor;

  console.log(`Total com frete: R$ ${total.toFixed(2)}`);
  console.log(`Previsão de entrega: ${frete.prazo}`);

  const pedido = await confirmarPedido({ itens: itensVerificados, total, usuario });
  return pedido;
}

Tente reescrever isso com .then() encadeados e você vai ver o quanto async/await ajuda.


Loops assíncronos

Async/await permite usar estruturas de controle normais com código assíncrono:

const ids = [1, 2, 3, 4, 5];

// ── Sequencial — aguarda cada um antes do próximo
async function carregarSequencial() {
  console.log("Carregando em sequência...");
  const resultados = [];

  for (const id of ids) {
    const usuario = await buscarUsuario(id); // aguarda antes de continuar
    resultados.push(usuario);
    console.log(`Carregado: ${usuario.nome}`);
  }

  return resultados;
}
// Tempo total: soma dos tempos individuais (~5s se cada um levar 1s)

// ── Paralelo — inicia todos ao mesmo tempo
async function carregarParalelo() {
  console.log("Carregando em paralelo...");
  const promises = ids.map(id => buscarUsuario(id));
  const resultados = await Promise.all(promises);
  resultados.forEach(u => console.log(`Carregado: ${u.nome}`));
  return resultados;
}
// Tempo total: tempo do mais lento (~1s)

Use sequencial quando cada operação depende da anterior. Use paralelo (Promise.all) quando as operações são independentes.


async/await com Promise.all

Você pode — e deve — combinar async/await com os métodos de Promise:

async function carregarDashboard(usuarioId) {
  // Inicia tudo em paralelo — eficiente
  const [usuario, pedidos, notificacoes, relatorio] = await Promise.all([
    buscarUsuario(usuarioId),
    buscarPedidos(usuarioId),
    buscarNotificacoes(usuarioId),
    gerarRelatorio(usuarioId),
  ]);

  return { usuario, pedidos, notificacoes, relatorio };
}

// Usando
async function main() {
  try {
    const dados = await carregarDashboard(42);
    console.log(`Bem-vindo, ${dados.usuario.nome}!`);
    console.log(`${dados.pedidos.length} pedidos encontrados.`);
    console.log(`${dados.notificacoes.length} notificações.`);
  } catch (erro) {
    console.error("Erro ao carregar dashboard:", erro.message);
  }
}

main();

Top-level await

No passado, await só podia ser usado dentro de funções async. Em módulos JavaScript modernos (.mjs ou com "type": "module" no package.json), você pode usar await diretamente no nível do módulo:

// Em um módulo ES moderno — sem precisar de função async
const dados = await fetch("https://api.exemplo.com/dados");
const json = await dados.json();
console.log(json);

Isso é muito útil em scripts de configuração e no Node.js moderno.


Erro comum — await em forEach

Um dos erros mais frequentes com async/await: usar await dentro de forEach não funciona como esperado:

const ids = [1, 2, 3];

// ❌ ERRADO — forEach não aguarda os awaits
async function errado() {
  ids.forEach(async (id) => {
    const usuario = await buscarUsuario(id);
    console.log(usuario.nome); // a ordem é imprevisível
  });
  console.log("Terminou?"); // executa ANTES dos awaits!
}

// ✅ CORRETO — use for...of para código sequencial
async function correto() {
  for (const id of ids) {
    const usuario = await buscarUsuario(id);
    console.log(usuario.nome); // ordem garantida
  }
  console.log("Terminou!"); // executa DEPOIS de todos
}

// ✅ CORRETO — use Promise.all para paralelo
async function paraleloCorreto() {
  const usuarios = await Promise.all(ids.map(id => buscarUsuario(id)));
  usuarios.forEach(u => console.log(u.nome));
  console.log("Terminou!");
}

Padrões avançados

Retry automático — tentar novamente em caso de falha:

async function comRetry(funcao, tentativas = 3, delay = 1000) {
  for (let i = 1; i <= tentativas; i++) {
    try {
      return await funcao();
    } catch (erro) {
      console.warn(`Tentativa ${i}/${tentativas} falhou: ${erro.message}`);

      if (i === tentativas) throw erro; // última tentativa — propaga o erro

      await new Promise(resolve => setTimeout(resolve, delay * i)); // espera antes de tentar de novo
    }
  }
}

// Uso
const dados = await comRetry(
  () => buscarDadosDaAPI(),
  3,    // 3 tentativas
  500,  // 500ms, 1000ms, 1500ms entre tentativas
);

Timeout em requisições:

async function comTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout após ${ms}ms`)), ms)
  );

  return Promise.race([promise, timeout]);
}

// Uso
try {
  const dados = await comTimeout(buscarDadosDaAPI(), 5000);
  console.log(dados);
} catch (erro) {
  console.error(erro.message); // "Timeout após 5000ms"
}

Exemplo completo — sistema de autenticação

// Simulando serviços
const authService = {
  async validarCredenciais(email, senha) {
    await new Promise(r => setTimeout(r, 800));
    if (email === "admin@email.com" && senha === "senha123") {
      return { token: "jwt_token_abc123", expiraEm: 3600 };
    }
    throw new Error("Credenciais inválidas.");
  },

  async obterPerfil(token) {
    await new Promise(r => setTimeout(r, 500));
    if (!token) throw new Error("Token ausente.");
    return {
      id: 1,
      nome: "Administrador",
      email: "admin@email.com",
      permissoes: ["ler", "escrever", "deletar"],
    };
  },

  async registrarAcesso(userId) {
    await new Promise(r => setTimeout(r, 200));
    const agora = new Date().toLocaleString("pt-BR");
    return { userId, acessoEm: agora, ip: "192.168.1.1" };
  },
};

// Função de login completa
async function login(email, senha) {
  console.log("🔐 Iniciando autenticação...");

  try {
    // Passo 1 — valida credenciais
    const auth = await authService.validarCredenciais(email, senha);
    console.log(`✅ Credenciais válidas. Token expira em ${auth.expiraEm}s`);

    // Passo 2 e 3 — busca perfil e registra acesso em paralelo
    const [perfil, registro] = await Promise.all([
      authService.obterPerfil(auth.token),
      authService.registrarAcesso(1),
    ]);

    console.log(`👤 Bem-vindo, ${perfil.nome}!`);
    console.log(`🔑 Permissões: ${perfil.permissoes.join(", ")}`);
    console.log(`📋 Acesso registrado em: ${registro.acessoEm}`);

    return { auth, perfil, registro };

  } catch (erro) {
    console.error(`❌ Falha no login: ${erro.message}`);
    throw erro;
  }
}

async function main() {
  // Login bem-sucedido
  await login("admin@email.com", "senha123");

  console.log("\n--- Tentativa com credenciais erradas ---\n");

  // Login com falha
  await login("hacker@email.com", "errada").catch(() => {
    console.log("Redirecionando para tela de erro...");
  });
}

main();

Resumo — quando usar cada abordagem

Situação Use
Código simples e linear async/await
Operações em paralelo async/await + Promise.all
Encadeamento simples .then() ou async/await
Eventos que ocorrem várias vezes Callbacks
Precisa de .race(), .any(), .allSettled() Combine com await
Loop que precisa ser sequencial for...of com await
Loop paralelo Promise.all + map com await

Tarefa para você

Construa um sistema de importação de dados em lotes com async/await:

// Contexto: você tem 50 usuários para importar para um banco de dados.
// Importar todos de uma vez pode sobrecarregar o servidor.
// Importe em lotes de 10, com 500ms de pausa entre os lotes.

async function importarEmLotes(usuarios, tamanhoDeLote = 10, pausaMs = 500) {
  // 1. Divida o array em lotes de 'tamanhoDeLote'
  // 2. Para cada lote, importe todos em paralelo (Promise.all)
  // 3. Aguarde 'pausaMs' antes do próximo lote
  // 4. Exiba o progresso: "Lote 1/5 concluído (10/50 usuários)"
  // 5. Retorne um relatório final: { sucesso, falha, total }
}

// Simule a função de importar um usuário:
async function importarUsuario(usuario) {
  await new Promise(r => setTimeout(r, Math.random() * 300 + 100));
  if (Math.random() < 0.1) throw new Error(`Falha ao importar ${usuario.nome}`);
  return { id: Math.random(), ...usuario };
}

Conclusão

Neste artigo você aprendeu:

  • O que async faz a uma função
  • Como await pausa a execução sem bloquear a thread
  • Como async/await melhora legibilidade em relação ao .then()
  • Tratamento de erros com try/catch/finally
  • Como usar lógica condicional e loops com código assíncrono
  • A combinação poderosa de async/await com Promise.all
  • O erro clássico do await dentro do forEach
  • Padrões avançados: retry e timeout
  • Quando usar cada abordagem

No próximo artigo colocamos tudo isso em prática com a Fetch API — buscando dados reais da internet em APIs públicas.

📚 Fontes e Referências

Comentários

Mais em Javascript

Testes automatizados com Jest
Testes automatizados com Jest

Existe um momento na vida de todo desenvolvedor em que ele mexe em um código...

Padrões de Projeto em JavaScript
Padrões de Projeto em JavaScript

Depois de anos resolvendo os mesmos tipos de problemas, a indústria de softwa...

Escopo, Hoisting e Closures
Escopo, Hoisting e Closures

Este &eacute; um dos artigos mais importantes da s&eacute;rie. N&atilde;o por...