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

Eventos: click, input, submit e muito mais
Eventos: click, input, submit e muito mais

No artigo anterior aprendemos a selecionar e modificar elementos do DOM. Mas...

Funções: declaração, expressão e arrow functions
Funções: declaração, expressão e arrow functions

Se os la&ccedil;os evitam a repeti&ccedil;&atilde;o de a&ccedil;&otilde;es, a...

Mini Projeto: Quiz Interativo
Mini Projeto: Quiz Interativo

Chegamos ao fim do M&oacute;dulo 2. Em sete artigos voc&ecirc; aprendeu a man...