async/await é uma sintaxe introduzida no ECMAScript 2017 (ES8) que simplifica o trabalho com Promises, tornando o código assíncrono mais legível e semelhante a código síncrono. Em vez de encadear .then() e .catch(), você pode escrever funções que 'pausam' a execução até que uma Promise seja resolvida ou rejeitada, facilitando o fluxo de controle.

Nesta aula, exploraremos como usar async/await para escrever código assíncrono de forma clara, como tratar erros adequadamente e como decidir entre execução sequencial e paralela para otimizar o desempenho.

Sintaxe

A palavra-chave async é colocada antes de uma declaração de função (seja ela tradicional, arrow ou expressão) para indicar que a função sempre retornará uma Promise. Dentro de uma função async, você pode usar await antes de uma expressão que retorna uma Promise, fazendo com que a execução da função espere até que a Promise seja resolvida (ou rejeitada).

Exemplo: converter uma função que retorna uma Promise para usar async/await.

// Função que retorna uma Promise (versão tradicional)
function buscarDados() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Dados carregados"), 1000);
  });
}

// Função assíncrona usando async/await
async function obterDados() {
  const resultado = await buscarDados();
  console.log(resultado); // "Dados carregados"
  return resultado;
}

obterDados();

Observe que obterDados é declarada com async e dentro dela usamos await para aguardar a Promise retornada por buscarDados. O valor resolvido é atribuído à variável resultado. A função obterDados também retorna uma Promise (implicitamente), que será resolvida com o valor de resultado.

É importante lembrar que await só pode ser usado dentro de funções async. Fora delas, você ainda precisa usar .then() ou encapsular em uma função assíncrona.

Tratamento de erro com try/catch

Uma das grandes vantagens do async/await é a capacidade de usar blocos try/catch para capturar erros de Promises rejeitadas, de forma similar ao tratamento de exceções síncronas. Se uma Promise rejeitada for aguardada com await, o erro será lançado como uma exceção, que pode ser capturada com catch.

Exemplo: simular uma requisição que pode falhar.

async function buscarUsuario(id) {
  if (id < 0) {
    throw new Error("ID inválido");
  }
  // Simula uma chamada de API
  return { id, nome: "João" };
}

async function exibirUsuario(id) {
  try {
    const usuario = await buscarUsuario(id);
    console.log("Usuário:", usuario);
  } catch (erro) {
    console.error("Erro ao buscar usuário:", erro.message);
  }
}

exibirUsuario(-1); // "Erro ao buscar usuário: ID inválido"
exibirUsuario(1);  // "Usuário: { id: 1, nome: 'João' }"

No exemplo, se buscarUsuario lançar uma exceção (ou retornar uma Promise rejeitada), o catch a capturará. Você também pode usar finally para executar código independentemente de sucesso ou falha.

É uma boa prática sempre tratar erros em funções assíncronas para evitar Promises rejeitadas não tratadas, que podem causar comportamentos inesperados ou vazamentos de memória.

Sequencial vs paralelo

Ao lidar com múltiplas operações assíncronas, você precisa decidir se elas devem ser executadas em sequência (uma após a outra) ou em paralelo (simultaneamente). O async/await facilita ambas as abordagens.

Execução sequencial: Use await para cada operação, uma após a outra. Isso é útil quando cada operação depende do resultado da anterior.

async function sequencial() {
  const a = await operacao1(); // 2 segundos
  const b = await operacao2(); // 2 segundos
  const c = await operacao3(); // 2 segundos
  // total: ~6 segundos
  return [a, b, c];
}

Execução paralela: Para iniciar todas as operações ao mesmo tempo, use Promise.all (ou Promise.allSettled) com um array de Promises. Você pode iniciar as Promises sem await e depois aguardar todas com await Promise.all().

async function paralelo() {
  const promise1 = operacao1(); // inicia imediatamente
  const promise2 = operacao2(); // inicia imediatamente
  const promise3 = operacao3(); // inicia imediatamente
  const resultados = await Promise.all([promise1, promise2, promise3]);
  // total: ~2 segundos (a mais lenta)
  return resultados;
}

Outra abordagem paralela é usar await em um array de Promises com Promise.all diretamente: const resultados = await Promise.all([operacao1(), operacao2(), operacao3()]);.

Escolha entre sequencial e paralelo com base na dependência entre as operações. Se não houver dependência, prefira paralelo para melhor performance.

Boas práticas e observações finais

  • Sempre trate erros com try/catch em funções async.
  • Não misture .then() com await desnecessariamente; escolha um estilo e mantenha consistência.
  • Evite usar await em loops forEach; prefira for...of ou Promise.all com map.
  • Use Promise.all para paralelismo e Promise.allSettled quando precisar de resultados parciais.
  • Lembre-se de que funções async sempre retornam uma Promise, então você pode encadear .then() se necessário.

Referências

Exercícios

  1. Crie uma função assíncrona chamada esperarESaudacao que recebe um nome e um tempo em milissegundos. A função deve aguardar o tempo especificado (usando setTimeout com Promise) e então exibir no console: "Olá, [nome]!"

    ✓ Resposta:
    function delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    async function esperarESaudacao(nome, tempo) {
      await delay(tempo);
      console.log(`Olá, ${nome}!`);
    }
    
    // Teste:
    esperarESaudacao("Maria", 1000); // após 1s: Olá, Maria!
  2. Escreva uma função assíncrona buscarDadosComErro que simula uma requisição que rejeita com a mensagem "Erro de rede". Use try/catch para capturar o erro e exibir no console "Erro capturado: [mensagem]".

    ✓ Resposta:
    async function buscarDadosComErro() {
      try {
        const dados = await new Promise((_, reject) =>
          setTimeout(() => reject(new Error("Erro de rede")), 500)
        );
        console.log(dados);
      } catch (erro) {
        console.log("Erro capturado:", erro.message);
      }
    }
    
    buscarDadosComErro(); // "Erro capturado: Erro de rede"
  3. Implemente uma função sequencial que aguarda três Promises que resolvem após 1, 2 e 3 segundos respectivamente, retornando um array com os valores resolvidos. Use await sequencial.

    ✓ Resposta:
    function criarPromise(valor, tempo) {
      return new Promise(resolve => setTimeout(() => resolve(valor), tempo));
    }
    
    async function sequencial() {
      const a = await criarPromise("A", 1000);
      const b = await criarPromise("B", 2000);
      const c = await criarPromise("C", 3000);
      return [a, b, c];
    }
    
    sequencial().then(console.log); // Após ~6s: ["A", "B", "C"]
  4. Reescreva a função do exercício 3 para executar as Promises em paralelo usando Promise.all. Qual o tempo total aproximado?

    ✓ Resposta:
    async function paralelo() {
      const promises = [
        criarPromise("A", 1000),
        criarPromise("B", 2000),
        criarPromise("C", 3000)
      ];
      return await Promise.all(promises);
    }
    
    paralelo().then(console.log); // Após ~3s: ["A", "B", "C"]
    // O tempo total é o da Promise mais lenta (3 segundos).
  5. Crie uma função assíncrona obterUsuarios que faz duas chamadas paralelas: uma para buscar uma lista de usuários (demora 2s) e outra para buscar permissões (demora 1s). Use Promise.all e retorne um objeto com ambas as listas. Simule as chamadas com Promises.

    ✓ Resposta:
    function buscarUsuarios() {
      return new Promise(resolve =>
        setTimeout(() => resolve(["João", "Maria"]), 2000)
      );
    }
    
    function buscarPermissoes() {
      return new Promise(resolve =>
        setTimeout(() => resolve(["admin", "user"]), 1000)
      );
    }
    
    async function obterUsuarios() {
      const [usuarios, permissoes] = await Promise.all([
        buscarUsuarios(),
        buscarPermissoes()
      ]);
      return { usuarios, permissoes };
    }
    
    obterUsuarios().then(console.log);
    // Após ~2s: { usuarios: ["João", "Maria"], permissoes: ["admin", "user"] }