Promises e async/await em JavaScript: como lidar com código assíncrono sem sofrer

Entenda o que são Promises e como async/await deixa o código assíncrono em JavaScript mais claro, com exemplos simples e os erros mais comuns.

Compartilhar no Linkedin Compartilhar no WhatsApp

Tempo estimado de leitura: 8 minutos

Imagem do artigo Promises e async/await em JavaScript: como lidar com código assíncrono sem sofrer

Buscar dados de uma API, ler um arquivo ou esperar a resposta de um servidor leva tempo. Se o JavaScript parasse tudo para aguardar cada uma dessas tarefas, a página inteira travaria. A solução é o código assíncrono, e as Promises junto com o async/await são as ferramentas modernas para escrevê-lo de forma legível.

O problema: tarefas que demoram

O JavaScript executa uma instrução por vez. Quando você pede dados a um servidor, a resposta não chega na hora. Em vez de bloquear a execução, a linguagem registra o pedido, segue com o restante do código e volta ao resultado quando ele estiver pronto.

Antigamente, isso era resolvido com callbacks: funções passadas como argumento para serem chamadas no fim da tarefa. O problema aparecia quando uma tarefa dependia da outra, e os callbacks iam se aninhando em camadas difíceis de ler, o famoso “callback hell”.

O que é uma Promise

Uma Promise (promessa) é um objeto que representa o resultado futuro de uma operação assíncrona. Pense em um pedido de comida: você recebe um comprovante agora e, mais tarde, o pedido chega ou dá algum problema. Uma Promise tem sempre um destes três estados:

  • Pendente (pending): a operação ainda está em andamento.
  • Resolvida (fulfilled): terminou com sucesso e entregou um valor.
  • Rejeitada (rejected): falhou e entregou um motivo de erro.

Depois de resolvida ou rejeitada, a Promise não muda mais de estado. Para reagir ao resultado, usamos os métodos .then() (sucesso), .catch() (erro) e .finally() (executa em ambos os casos).

fetch("https://exemplo.com/api/usuarios")
  .then(resposta => resposta.json())
  .then(dados => console.log(dados))
  .catch(erro => console.error("Falhou:", erro))
  .finally(() => console.log("Fim da requisição"));

Cada .then() devolve uma nova Promise, o que permite encadear etapas em sequência sem aninhar funções. Já é bem melhor do que callbacks, mas ainda dá para deixar mais limpo.

async/await: Promises com cara de código comum

O async/await é uma sintaxe que funciona por cima das Promises. Duas regras bastam para começar:

  1. Uma função marcada com async sempre devolve uma Promise.
  2. Dentro dela, o await pausa a execução daquela função até a Promise ser resolvida e devolve o valor final.
async function carregarUsuarios() {
  try {
    const resposta = await fetch("https://exemplo.com/api/usuarios");
    const dados = await resposta.json();
    console.log(dados);
  } catch (erro) {
    console.error("Falhou:", erro);
  } finally {
    console.log("Fim da requisição");
  }
}

O resultado faz exatamente o mesmo que o exemplo anterior, mas lê-se de cima para baixo, como um código síncrono. O tratamento de erros também fica familiar: o bloco try/catch captura tanto falhas de rede quanto erros lançados dentro da função.

Um detalhe importante: o await pausa apenas a função em que está. O restante da aplicação continua funcionando normalmente, e a página não congela.

Executando tarefas em paralelo com Promise.all

Um erro comum é usar await em sequência para tarefas que não dependem uma da outra. Isso faz uma esperar pela outra sem necessidade:

// Sequencial: a segunda só começa quando a primeira termina
const usuarios = await buscarUsuarios();
const produtos = await buscarProdutos();

// Paralelo: as duas começam juntas
const [usuarios2, produtos2] = await Promise.all([
  buscarUsuarios(),
  buscarProdutos()
]);

O Promise.all recebe uma lista de Promises e devolve uma nova Promise que resolve quando todas terminam. Se uma delas for rejeitada, o conjunto inteiro é rejeitado.

Comparando as três abordagens

AbordagemComo funcionaPrincipal vantagemPrincipal limitação
CallbacksFunção chamada ao fim da tarefaSimples para casos pequenosAninhamento difícil de ler
Promises (.then)Encadeamento de etapasFluxo em cadeia e erro centralizadoCadeias longas ainda cansam
async/awaitEspera com sintaxe sequencialLeitura natural e try/catchExige atenção para não serializar tarefas

Erros comuns e como evitá-los

  • Esquecer o await: sem ele, você recebe a Promise em si, e não o valor. Se aparecer Promise { <pending> } no console, é um bom sinal de que o await ficou de fora.
  • Não tratar erros: uma Promise rejeitada sem catch gera um erro não tratado. Use sempre try/catch ou .catch().
  • Esquecer de verificar a resposta do fetch: o fetch só rejeita em falhas de rede. Respostas como 404 ou 500 não são rejeitadas automaticamente, então vale checar resposta.ok.
  • Usar await dentro de loops sem necessidade: quando as tarefas são independentes, prefira Promise.all para ganhar tempo.
  • Usar await fora de função async: em scripts comuns, isso causa erro de sintaxe. Em módulos modernos, o await no nível superior é permitido.

Quando usar cada ferramenta

Na prática, o async/await é a escolha mais comum no dia a dia, porque deixa o fluxo claro. As Promises com .then() continuam úteis em trechos curtos e em bibliotecas que devolvem Promises. E é útil saber ler os dois estilos, já que você os encontrará misturados em projetos reais.

Um exemplo prático: carregar dados e mostrar na tela

Imagine uma página que precisa exibir uma lista de tarefas vinda de um servidor. O fluxo costuma ter quatro passos: mostrar uma mensagem de carregamento, pedir os dados, tratar uma eventual falha e, por fim, desenhar o resultado. Com async/await, cada passo ocupa uma linha ou duas e a ordem fica evidente para quem lê o código.

async function mostrarTarefas() {
  const lista = document.querySelector("#tarefas");
  lista.textContent = "Carregando...";
  try {
    const resposta = await fetch("/api/tarefas");
    if (!resposta.ok) throw new Error("Erro " + resposta.status);
    const tarefas = await resposta.json();
    lista.innerHTML = tarefas.map(t => "<li>" + t.titulo + "</li>").join("");
  } catch (erro) {
    lista.textContent = "Não foi possível carregar as tarefas.";
  }
}

Repare que a verificação de resposta.ok transforma um erro HTTP em uma exceção, que cai direto no catch. Assim, o usuário sempre vê uma mensagem clara, seja qual for o motivo da falha. Esse padrão de carregar, validar e exibir aparece em quase toda aplicação web que consome uma API, e dominá-lo já resolve boa parte dos casos reais.

Conclusão

Código assíncrono deixa de ser um mistério quando você entende que uma Promise é só um objeto que representa um resultado futuro, e que o async/await é uma forma mais agradável de esperar por ele. Com try/catch para erros e Promise.all para paralelismo, você já resolve a maioria dos casos do dia a dia.

Se você quer praticar esses conceitos com projetos reais, vale conhecer os cursos de programação front-end e de JavaScript da Cursa, que levam você do básico ao consumo de APIs passo a passo.

Cookies, Cache e Histórico: O Que o Navegador Guarda Sobre Você

Entenda o que são cookies, cache e histórico do navegador, para que servem, quando limpar cada um e como isso afeta sua privacidade.

Sensores e Atuadores: Como o Arduino Percebe o Mundo e Age Sobre Ele

Entenda a diferença entre sensores e atuadores, como o Arduino lê sinais e aciona dispositivos, e como montar seus primeiros projetos.

Endereço IP, Máscara de Sub-rede e Gateway: O Trio Que Faz Sua Rede Funcionar

Entenda de forma prática o que são endereço IP, máscara de sub-rede e gateway padrão, e como esses três parâmetros conversam entre si.

Compressão de Arquivos: Como o ZIP Consegue Reduzir o Tamanho dos Seus Dados

Entenda como funciona a compressão de arquivos, a diferença entre métodos com e sem perda e quando cada formato faz sentido.

Expressões Regulares (Regex): O Que São e Como Usar na Prática

Aprenda o que são expressões regulares, os principais símbolos, quantificadores e como aplicar regex para buscar e validar textos.

Como Funciona o DNS: o Sistema que Traduz Nomes em Endereços na Internet

Entenda de forma simples o que é DNS, como acontece a resolução de nomes, os principais tipos de registro e por que ele é essencial para a internet.

INNER JOIN, LEFT JOIN e RIGHT JOIN: como relacionar tabelas no SQL

Entenda de forma simples como funcionam INNER JOIN, LEFT JOIN, RIGHT JOIN e FULL JOIN para combinar dados de várias tabelas no SQL.

TypeScript para iniciantes: o que é e por que ele melhora o JavaScript

Entenda o que é TypeScript, como a tipagem estática ajuda a evitar erros e por que ele se tornou essencial no desenvolvimento web moderno.