SkillsTecnológicas
Menu
Back-end

Async/Await e Promises: Erros, Concorrência e Boas Práticas

Domine Async/Await e Promises no JavaScript: encadeamento, erros, concorrência, fetch, cancelamento e boas práticas com exemplos práticos.

Marcos RodriguesPublicado em 27 de julho de 2024Atualizado em 18 de agosto de 202616 min de leitura
Fluxo assíncrono em JavaScript com fila de tarefas e caminhos de sucesso e erro

Async/Await e Promises são duas formas de trabalhar com o mesmo modelo assíncrono do JavaScript.

Uma Promise representa um resultado que poderá chegar depois; async faz uma função retornar uma Promise; e await suspende apenas aquela função até o resultado ser cumprido ou rejeitado, sem bloquear toda a aplicação.

O desafio real não é decorar a sintaxe. É saber quando iniciar tarefas em sequência ou de forma concorrente, como propagar falhas, verificar respostas HTTP, cancelar trabalho obsoleto e evitar Promises sem tratamento.

Esses detalhes diferenciam um exemplo que “funciona” de um fluxo confiável em produção.

Este guia aprofunda erros, concorrência e boas práticas. Se você ainda está conhecendo callback, event loop e a ideia geral de assincronicidade, leia primeiro a introdução a JavaScript assíncrono com exemplos.

Aqui partimos dos fundamentos para resolver cenários que aparecem em interfaces, APIs e aplicações Node.js.

Async/Await e Promises: qual é a diferença?

Promise é o objeto que representa a conclusão futura de uma operação. Async/Await é uma sintaxe para consumir e combinar operações baseadas em Promises.

Eles não são concorrentes: quando você usa await buscarDados(), a função chamada normalmente devolve uma Promise, e a função marcada com async também devolve outra.

RecursoPapelUso comum
PromiseRepresenta um resultado futuroCompor tarefas e expor APIs assíncronas
.then()Registra continuação para cumprimentoEncadeamento e transformação funcional
.catch()Registra tratamento para rejeiçãoRecuperação ou conversão de erro
asyncFaz a função sempre retornar uma PromiseDefinir fluxo assíncrono legível
awaitObtém cumprimento ou lança rejeiçãoEscrever dependências em sequência

Uma base sólida de funções, escopo e objetos continua importante. O guia sobre a linguagem JavaScript e seu ecossistema organiza esses fundamentos antes de entrar em padrões assíncronos.

O que são Promises?

Uma Promise é um objeto que permite associar ações ao resultado futuro de uma operação.

A referência de Promise da MDN descreve o objeto como representante da conclusão ou falha eventual de uma operação assíncrona e do valor resultante.

Ela não torna uma operação assíncrona por si só. O que faz o trabalho pode ser uma requisição de rede, um temporizador, uma API do navegador, acesso a arquivos no servidor ou uma biblioteca.

A Promise padroniza como o chamador observa o término e compõe o próximo passo.

Estados e resolução de uma Promise

  • Pendente: ainda não existe resultado final.
  • Cumprida: terminou com um valor, inclusive undefined.
  • Rejeitada: terminou com um motivo de falha, preferencialmente um objeto Error.

Depois de cumprida ou rejeitada, a Promise está resolvida de forma definitiva.

Chamadas posteriores a resolve ou reject não mudam o resultado. Uma Promise também pode ser resolvida com outra Promise; nesse caso, acompanha o estado dela antes de produzir o resultado final.

Ciclo de uma Promise passando do estado pendente para concluída ou rejeitada
Uma Promise começa pendente e, quando termina, segue por um único caminho: cumprimento ou rejeição.

Criando e utilizando Promises

Use o construtor quando precisar adaptar uma API baseada em callback ou controlar explicitamente o momento da conclusão.

O executor é chamado imediatamente e de forma síncrona; o resultado futuro é comunicado por resolve ou reject.

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

esperar(500).then(() => {
  console.log("Tarefa concluída");
});

Não envolva uma função que já retorna Promise em new Promise sem necessidade. Esse antipadrão adiciona código, pode engolir rejeições e dificulta leitura. Retorne a Promise original ou transforme o resultado com then.

Como funciona o encadeamento

Cada chamada a then, catch ou finally devolve uma nova Promise. Isso permite transformar valores e propagar falhas.

A regra central é retornar o próximo valor ou a próxima Promise. Se você inicia uma tarefa dentro de then e não a retorna, o restante da cadeia não espera por ela.

buscarUsuario(id)
  .then((usuario) => buscarPedidos(usuario.id))
  .then((pedidos) => calcularResumo(pedidos))
  .then((resumo) => exibirResumo(resumo))
  .catch((erro) => registrarFalha(erro))
  .finally(() => encerrarCarregamento());

Um valor retornado é usado para cumprir a próxima Promise. Uma Promise retornada é aguardada. Uma exceção lançada rejeita a próxima etapa.

A documentação da MDN sobre uso de Promises enfatiza retorno e composição como base para evitar cadeias quebradas e o aninhamento conhecido como “callback hell”.

Como funciona Async/Await

Uma função async sempre retorna uma Promise. Se retorna um valor comum, a Promise é cumprida com esse valor.

Se lança uma exceção não capturada, a Promise é rejeitada. A referência de funções async mostra que await permite usar try/catch com operações baseadas em Promises.

async function carregarResumo(id) {
  const usuario = await buscarUsuario(id);
  const pedidos = await buscarPedidos(usuario.id);
  return calcularResumo(pedidos);
}

carregarResumo(42)
  .then(exibirResumo)
  .catch(registrarFalha);

Async/Await melhora a leitura quando uma etapa depende do resultado anterior. Não transforma o código em síncrono nem elimina a necessidade de tratar a Promise que a função devolve.

O que o await realmente pausa

await suspende a continuação da função assíncrona até a expressão ser resolvida. Enquanto isso, o chamador já recebeu uma Promise e o runtime pode processar eventos e outras tarefas.

Ele não bloqueia a thread principal nem pausa o restante do programa.

Isso não resolve trabalho pesado de CPU. Um cálculo longo executado em JavaScript ainda pode ocupar a thread.

Browser Workers, processos separados ou outra arquitetura podem ser necessários.

Assincronicidade melhora espera por I/O; não cria paralelismo automático para qualquer código.

Tratamento e propagação de erros

Uma rejeição precisa ser observada por catch, por try/catch ao redor de um await ou pelo chamador da função. Prefira rejeitar com Error ou uma classe derivada, pois mensagem, causa e stack facilitam diagnóstico.

async function salvarPerfil(dados) {
  try {
    return await api.salvar(dados);
  } catch (erro) {
    throw new Error("Não foi possível salvar o perfil", {
      cause: erro,
    });
  }
}

Evite capturar tudo e devolver um valor neutro silenciosamente. O chamador pode interpretar uma lista vazia como sucesso e armazenar informação incorreta.

Se existe recuperação real, aplique-a; caso contrário, acrescente contexto e propague.

Quando capturar e quando relançar

  • Capture quando consegue recuperar, traduzir a falha para o domínio, liberar recursos ou apresentar uma resposta apropriada.
  • Relance quando a camada atual não sabe resolver, mas pode adicionar contexto útil.
  • Deixe propagar quando o erro já carrega contexto suficiente e existe tratamento centralizado.
  • Use finally para limpeza que deve ocorrer tanto no sucesso quanto na falha, sem substituir o resultado.

Ao investigar rejeições difíceis, consulte também o guia para depurar erros em aplicações Node.js, que aborda logs, stack traces e inspeção do ambiente.

Execução sequencial e concorrente

Duas chamadas com await em linhas consecutivas podem ser obrigatoriamente sequenciais ou apenas acidentalmente lentas.

Se a segunda depende da primeira, a sequência é correta. Se são independentes, inicie ambas antes de esperar.

// Sequencial: a segunda chamada depende da primeira
const usuario = await buscarUsuario(id);
const pedidos = await buscarPedidos(usuario.id);

// Concorrente: as duas chamadas são independentes
const usuarioPromise = buscarUsuario(id);
const categoriasPromise = buscarCategorias();

const [usuarioAtual, categorias] = await Promise.all([
  usuarioPromise,
  categoriasPromise,
]);
Comparação visual entre execução sequencial, concorrente e cancelamento de tarefas assíncronas
Tarefas dependentes pedem sequência; tarefas independentes podem ser iniciadas juntas, com tratamento e cancelamento planejados.

Promise.all: todas precisam funcionar

Promise.all cumpre quando todas cumprem e preserva a ordem de entrada nos resultados, não a ordem de conclusão.

Rejeita assim que uma delas rejeita. Isso é ideal quando o resultado completo só faz sentido com todas as partes.

O comportamento “fail fast” não cancela as outras operações. Elas podem continuar executando e produzindo efeitos.

Se cancelamento for importante, cada API precisa receber um sinal ou oferecer outro mecanismo explícito.

Promise.allSettled: inspecione todos os resultados

Promise.allSettled espera todas terminarem e devolve objetos com estado e valor ou motivo.

Use quando falhas parciais são aceitáveis, como processar vários arquivos, consultar integrações independentes ou gerar um relatório completo de sucessos e erros.

const resultados = await Promise.allSettled(
  clientes.map((cliente) => sincronizar(cliente))
);

const falhas = resultados.filter(
  (resultado) => resultado.status === "rejected"
);

Promise.any e Promise.race

Promise.any cumpre com o primeiro sucesso; se todas rejeitarem, rejeita com AggregateError. Pode servir para obter um recurso equivalente por fontes redundantes. Promise.race acompanha a primeira Promise a terminar, seja com sucesso ou falha.

É útil para competir sinais, mas também não cancela as perdedoras.

Escolha pelo significado do negócio. Usar race apenas para “pegar a mais rápida” pode deixar requisições, temporizadores ou efeitos em andamento.

Como limitar a concorrência

Promise.all(itens.map(processar)) inicia tudo de uma vez. Com milhares de itens, isso pode abrir conexões demais, consumir memória ou atingir limites de uma API.

Use lotes, uma fila ou um conjunto pequeno de workers assíncronos.

async function processarEmLotes(itens, tamanho = 5) {
  const resultados = [];

  for (let inicio = 0; inicio < itens.length; inicio += tamanho) {
    const lote = itens.slice(inicio, inicio + tamanho);
    resultados.push(...(await Promise.all(lote.map(processar))));
  }

  return resultados;
}

Lotes introduzem uma barreira entre grupos. Um pool de workers usa melhor a capacidade quando tarefas têm durações diferentes.

A escolha depende de limite externo, memória e ordem exigida.

Loops assíncronos sem armadilhas

Use for...of com await quando cada iteração depende da anterior ou quando precisa limitar a taxa a uma tarefa.

Use map mais Promise.all quando as tarefas são independentes e o volume é seguro.

Para fluxos assíncronos que produzem valores ao longo do tempo, for await...of consome iteráveis assíncronos.

Por que forEach não espera callbacks async

forEach ignora o valor retornado pelo callback. Marcar o callback como async cria Promises, mas o forEach termina sem aguardá-las. Isso pode encerrar a função cedo e deixar rejeições sem tratamento.

// Não aguarda as operações
itens.forEach(async (item) => {
  await salvar(item);
});

// Sequencial
for (const item of itens) {
  await salvar(item);
}

// Concorrente
await Promise.all(itens.map((item) => salvar(item)));

Requisição e resposta com fetch

fetch retorna uma Promise cumprida com um objeto Response quando os cabeçalhos da resposta chegam. Ler JSON, texto ou arquivo é outra operação assíncrona. O corpo normalmente só pode ser consumido uma vez.

async function buscarProduto(id, signal) {
  const resposta = await fetch(`/api/produtos/${id}`, { signal });

  if (!resposta.ok) {
    throw new Error(`Falha HTTP: ${resposta.status}`);
  }

  return resposta.json();
}

Se quiser praticar com um endpoint real e simples, o artigo sobre como consumir uma API de CEP em JavaScript oferece um cenário complementar. Em projetos maiores, contratos, validação e autenticação devem acompanhar o fluxo assíncrono.

fetch não rejeita automaticamente em erro HTTP

A documentação de uso da Fetch API explica que respostas como 404 e 500 cumprem a Promise com um Response. O código precisa verificar response.ok ou status.

A Promise rejeita em situações como falha de rede, URL inválida ou cancelamento.

Também não assuma que todo erro devolve JSON. Uma camada de cliente pode observar o cabeçalho de conteúdo, limitar tamanho e produzir um erro de domínio com status, corpo seguro e identificador de correlação.

Cancelamento com AbortController

Promises não possuem um método genérico que interrompa o trabalho subjacente.

A operação precisa aceitar cancelamento. No navegador e em APIs compatíveis, AbortController produz um signal que pode ser passado ao fetch.

const controller = new AbortController();

const carregamento = buscarProduto(42, controller.signal);

// Quando a tela muda ou a resposta deixa de ser necessária
controller.abort();

try {
  await carregamento;
} catch (erro) {
  if (erro.name !== "AbortError") throw erro;
}

Cancelar evita atualizar uma interface desmontada, desperdiçar banda ou manter trabalho que perdeu relevância.

O servidor ainda pode ter recebido e executado a requisição; cancelamento do cliente não equivale a desfazer um efeito já confirmado.

Timeout controlado para requisições

Um timeout deve abortar a operação, limpar o temporizador e produzir um erro distinguível.

Apenas competir fetch com um temporizador em Promise.race deixa a requisição original em andamento.

async function fetchComTimeout(url, ms) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);

  try {
    return await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timer);
  }
}

Event loop e fila de microtarefas

Callbacks de Promises e a continuação depois de await entram na fila de microtarefas.

Elas são executadas depois que a pilha atual termina e antes de o runtime avançar para certas tarefas seguintes, como temporizadores.

Por isso, uma Promise já cumprida ainda continua de forma assíncrona.

console.log("início");

setTimeout(() => console.log("temporizador"), 0);
Promise.resolve().then(() => console.log("promise"));

console.log("fim");

// Ordem: início, fim, promise, temporizador

Conhecer essa ordem ajuda a depurar estado atualizado “depois”, testes que terminam cedo e loops que criam microtarefas demais.

Também explica por que adicionar await pode mudar a sequência observável mesmo quando o valor já está disponível.

Erros comuns com Async/Await e Promises

  • Esquecer await ou return: o chamador recebe uma Promise quando esperava o valor, ou a cadeia avança antes da tarefa.
  • Capturar e ignorar: a falha desaparece e o sistema continua com estado incompleto.
  • Usar await em tarefas independentes: cria latência sequencial desnecessária.
  • Usar Promise.all sem limite: dispara mais trabalho do que a infraestrutura suporta.
  • Confiar que fetch rejeita em 404: trata resposta de erro como sucesso.
  • Usar forEach com callback async: o loop não aguarda as Promises.
  • Criar new Promise ao redor de outra Promise: aumenta complexidade e pode quebrar propagação.
  • Iniciar e esquecer: rejeições ficam sem observador e efeitos continuam fora do ciclo esperado.
  • Usar race como cancelamento: a tarefa perdedora permanece em execução.
  • Confundir assincronicidade com CPU paralela: um cálculo pesado ainda pode bloquear a thread.

Como testar e depurar código assíncrono

O teste deve aguardar a Promise e verificar os dois caminhos. Para sucesso, confirme valor e efeitos.

Para rejeição, confirme tipo e contexto do erro. Simule resposta lenta, timeout, cancelamento, erro HTTP e dados inválidos. Não dependa de esperas arbitrárias quando é possível controlar a Promise ou o relógio do teste.

  • retorne ou aguarde a Promise no teste;
  • use dependências injetáveis para substituir rede e relógio;
  • registre começo, fim, duração e identificador da operação;
  • preserve a causa original ao adicionar contexto;
  • teste concorrência com resultados em ordens diferentes;
  • confirme que cancelamento não aparece como falha inesperada;
  • evite dados sensíveis em logs.

Em APIs, códigos de status e mensagens devem ser definidos pelo contrato, não pelo detalhe interno de uma biblioteca.

O guia de desenvolvimento de APIs do planejamento à produção mostra como ligar esse tratamento à validação, segurança e observabilidade.

Padrões de produção

  • Tenha uma camada de cliente: centralize URL, autenticação, verificação de status e transformação de erros.
  • Propague sinais: funções intermediárias devem aceitar e encaminhar cancelamento.
  • Defina limites: timeout, concorrência, tamanho de resposta e tentativas precisam ser explícitos.
  • Repita somente o que é seguro: uma nova tentativa pode duplicar pagamentos, mensagens ou gravações.
  • Use idempotência: operações críticas devem tolerar repetição planejada.
  • Separe erro esperado de defeito: validação, indisponibilidade e bug exigem respostas diferentes.
  • Observe latência: meça dependências e não apenas o tempo total da rota.
  • Evite estado obsoleto: descarte respostas antigas quando uma solicitação mais recente já venceu.

Async/Await com TypeScript

TypeScript ajuda a tornar o contrato visível. Uma função async que produz um usuário deve retornar Promise<Usuario>.

Modele respostas de sucesso e erro, valide dados externos em runtime e evite usar any justamente na fronteira mais incerta.

type Produto = {
  id: number;
  nome: string;
};

async function buscarProduto(id: number): Promise<Produto> {
  const resposta = await fetch(`/api/produtos/${id}`);
  if (!resposta.ok) throw new Error("Produto indisponível");

  const dados: unknown = await resposta.json();
  return validarProduto(dados);
}

A tipagem não prova que a API externa respeitou o contrato. Ela orienta o código depois da validação. O artigo de tipagem em TypeScript aprofunda unknown, narrowing, generics e validação em runtime.

Promise ou Async/Await: qual usar?

Use Async/Await quando o fluxo tem etapas dependentes, múltiplas condições ou tratamento com try/catch/finally.

Use encadeamento quando uma transformação curta fica mais clara com then, quando deseja retornar a cadeia diretamente ou ao combinar APIs funcionais. Métodos estáticos como Promise.all continuam essenciais dentro de funções async.

Consistência importa mais que preferência estética. Não misture estilos sem motivo em uma única função. O objetivo é tornar dependências, concorrência e erros óbvios para quem revisa o código.

Exemplo prático completo

O exemplo abaixo busca perfil e permissões de forma concorrente, aplica timeout compartilhado, verifica HTTP e preserva contexto de erro. As duas respostas são necessárias, portanto Promise.all representa a regra do fluxo.

async function carregarSessao(usuarioId, timeoutMs = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const [perfilResposta, permissoesResposta] = await Promise.all([
      fetch(`/api/usuarios/${usuarioId}`, {
        signal: controller.signal,
      }),
      fetch(`/api/usuarios/${usuarioId}/permissoes`, {
        signal: controller.signal,
      }),
    ]);

    if (!perfilResposta.ok || !permissoesResposta.ok) {
      throw new Error("Não foi possível carregar a sessão");
    }

    const [perfil, permissoes] = await Promise.all([
      perfilResposta.json(),
      permissoesResposta.json(),
    ]);

    return { perfil, permissoes };
  } catch (erro) {
    if (erro.name === "AbortError") {
      throw new Error("Tempo limite ao carregar a sessão", {
        cause: erro,
      });
    }

    throw erro;
  } finally {
    clearTimeout(timer);
  }
}

Em produção, a validação dos JSONs, a estratégia de autenticação e os logs também fariam parte da camada. Se uma resposta fosse opcional, allSettled ou tratamento individual poderiam representar melhor o requisito.

Checklist de revisão

  • A função deixa claro que retorna uma Promise.
  • Toda Promise iniciada é retornada, aguardada ou tratada deliberadamente.
  • Tarefas independentes não foram serializadas sem motivo.
  • A concorrência possui limite compatível com o serviço.
  • Promise.all, allSettled, any ou race representa a regra real.
  • Erros são objetos com contexto e causa preservada.
  • fetch verifica response.ok antes de consumir o corpo.
  • Operações obsoletas podem ser canceladas quando a API suporta.
  • Timeout interrompe o trabalho e limpa recursos.
  • Loops usam for...of ou Promise.all, não forEach acidental.
  • Testes cobrem sucesso, rejeição, timeout, cancelamento e ordem variável.
  • Logs não expõem tokens, dados pessoais ou corpos sensíveis.

Perguntas frequentes

Async/Await substitui Promises?

Não. Async/Await é uma sintaxe construída sobre Promises. Funções async retornam Promises, await consome valores baseados nelas e métodos como Promise.all continuam necessários para concorrência.

await bloqueia o JavaScript?

Ele suspende apenas a continuação da função async. A thread pode processar outros eventos enquanto a Promise está pendente. Código pesado de CPU ainda bloqueia se for executado na thread principal.

Quando usar Promise.all?

Quando tarefas independentes podem começar juntas e o resultado completo exige que todas funcionem. Se precisa inspecionar sucessos e falhas individuais, considere Promise.allSettled. Controle o volume antes de iniciar uma lista grande.

Como cancelar uma Promise?

A Promise genérica não cancela o trabalho. A operação subjacente precisa aceitar um mecanismo, como AbortSignal no fetch. Rejeitar ou ignorar a Promise não garante que rede, timer ou efeito tenha parado.

Conclusão

Dominar Async/Await e Promises significa modelar tempo, dependências e falhas de forma explícita.

Promise representa o resultado futuro; Async/Await torna a composição mais legível; e os combinadores expressam como várias tarefas devem terminar.

Antes de otimizar, pergunte se as etapas dependem umas das outras. Depois escolha sequência, concorrência e limite, propague erros com contexto, verifique respostas HTTP e cancele o trabalho que perdeu utilidade.

Essa disciplina reduz condições de corrida, rejeições esquecidas e fluxos que falham apenas sob carga.