Promise.all: Guia Completo para Dominar promise.all no Ecossistema JavaScript

Pre

No universo de JavaScript, a forma como lidamos com várias operações assíncronas ao mesmo tempo pode definir a eficiência e a experiência do usuário. Entre as ferramentas mais poderosas para gerenciar múltiplas promessas simultâneas está o Promise.all, um método do objeto Promise que permite combinar várias promessas em uma única, simplificando fluxos de controle e evitando callbacks aninhados. Neste artigo, vamos explorar em profundidade o Promise.all, incluindo conceitos, casos de uso, comparações com outras abordagens, exemplos de código práticos e as melhores práticas para extrair o máximo desse recurso.

O que é Promise.all e por que ele importa

Promise.all é um método estático do construtor Promise que recebe, como argumento, um iterável de promessas (geralmente um array). Ele retorna uma nova promessa que se resolve quando todas as promessas do iterável forem resolvidas, fornecendo um array com os resultados na mesma ordem em que as promessas foram passadas. Se qualquer uma das promessas rejeitar, a promessa resultante é rejeitada imediatamente com o motivo da primeira rejeição.

Essa abordagem é especialmente útil quando precisamos buscar dados ou realizar operações independentes de forma paralela e só prosseguir quando todas estiverem concluídas. Em vez de encadear várias chamadas assíncronas com várias camadas de then/catch, o Promise.all oferece uma forma elegante e legível de sincronizar múltiplos trabalhos paralelos.

Como funciona Promise.all: funcionamento técnico e fluxo de execução

Ao iniciar, Promise.all começa lendo o iterável de promessas. Em seguida:

  • Cria um array de resultados com o mesmo tamanho do input; cada posição corresponde ao resultado da promessa respectiva.
  • Inicia todas as promessas em paralelo (se já forem promessas, ou os valores são convertidos em promessas resolvidas automaticamente).
  • Mantém um contador de promessas resolvidas. A cada resolução, armazena o resultado no índice correspondente e incrementa o contador.
  • Se todas as promessas forem resolvidas, resolve a promessa retornando o array de resultados, preservando a ordem original.
  • Se qualquer promessa for rejeitada, rejeita imediatamente com o motivo da primeira falha encontrada, não importa o estado das demais promessas.

Essa semântica explica por que a a saída é preservada na mesma ordem de entrada, mesmo que as operações se concluam em tempos diferentes. Além disso, a capacidade de falhar rápido é útil para cenários em que uma falha deve abortar trempe de operações dependentes.

Promise.all vs Promise.allSettled vs Promise.race vs Promise.any: quando usar cada um

Existem várias estratégias para combinar resultados de várias promessas. Conhecer as diferenças ajuda a escolher a API mais adequada ao problema.

Promise.all

Usado quando precisamos de todos os resultados antes de prosseguir e não queremos continuar se alguma operação falhar. Retorna um array com os resultados na mesma ordem. Útil para paralelizar chamadas independentes que são necessárias juntas.

Promise.allSettled

Retorna todas as promessas, independentemente de serem resolvidas ou rejeitadas. O resultado é um array de objetos que descrevem o estado de cada promessa (status, value ou reason). Ideal quando queremos saber o resultado de todas as operações, inclusive as que falharam, sem abortar o restante.

Promise.race

Resolve ou rejeita com o primeiro produtor de promessa que se cumprir ou falhar. Útil para cenários de timeout, limites de tempo ou quando apenas o primeiro resultado disponível importa.

Promise.any

Resolve com o primeiro valor que for bem-sucedido entre as promessas; se todas rejeitarem, a promessa resultante é rejeitada com um aggregate de falhas. Útil quando queremos tolerar falhas parciais, desde que haja pelo menos uma resposta válida.

Resumo rápido: Promise.all para esperar todos; Promise.allSettled para observar tudo; Promise.race para o primeiro resultado; Promise.any para o primeiro sucesso entre muitos. A escolha depende do comportamento desejado diante de falhas ou tempos de resposta.

Exemplos práticos com Promise.all

Exemplo 1: busca paralela de dados de diferentes endpoints

async function fetchAllUserData(userId) {
  const [profile, posts, comments] = await Promise.all([
    fetch(`/api/users/${userId}/profile`).then(res => res.json()),
    fetch(`/api/users/${userId}/posts`).then(res => res.json()),
    fetch(`/api/users/${userId}/comments`).then(res => res.json()),
  ]);
  return { profile, posts, comments };
}

Neste exemplo, três chamadas de rede são executadas em paralelo. O resultado é retornado como um objeto com as três fatias de dados, mantendo a ordem correspondente aos inputs.

Exemplo 2: carregamento de múltiplos recursos com verificação de erros

async function loadResources(urls) {
  try {
    const results = await Promise.all(urls.map(u => fetch(u).then(r => r.ok ? r.json() : Promise.reject('Erro ao carregar ' + u))));
    // results é um array com os dados na mesma ordem de urls
    return results;
  } catch (err) {
    // tratar falha geral
    console.error('Falha ao carregar recursos:', err);
    throw err;
  }
}

Este padrão mostra como combinar Promise.all com verificação de sucesso de cada resposta. Caso qualquer fetch falhe, a promessa all rejeita, permitindo tratamento central de erros.

Exemplo 3: uso junto com Promise.allSettled para tolerância a falhas

async function loadAllOrPartial(urls) {
  const results = await Promise.allSettled(urls.map(u => fetch(u).then(r => r.json())));
  const ok = results
    .map((r, i) => (r.status === 'fulfilled' ? { index: i, value: r.value } : null))
    .filter(x => x !== null);
  const errors = results.map((r, i) => r.status === 'rejected' ? i : null).filter(x => x !== null);
  return { ok, errors };
}

Neste caso, mesmo que algumas promessas falhem, ainda podemos processar as que deram certo. É útil quando a aplicação pode aproveitar dados disponíveis sem perder o restante das informações.

Boas práticas ao usar Promise.all

1. Garantir que todas as promessas são correspondentes aos mesmos inputs

Ao usar Promise.all, é fundamental que o array de promessas tenha correspondência entre cada índice e o resultado esperado. Qualquer desalinhamento pode levar a resultados embaralhados ou difíceis de manter.

2. Tratar erros de forma previsível

Como Promise.all rejeita na primeira falha, planeje o tratamento de erros em blocos try/catch quando usar await, ou use .catch para cada promessa se desejar tratamento individual antes de combinar com all.

3. Considerar timeouts com AbortController

Operações de rede podem levar muito tempo. Use AbortController para cancelar várias promessas simultâneas quando um tempo limite é atingido, evitando desperdício de recursos.

4. Evitar promessas desnecessárias

Se possível, use apenas promessas que realmente necessitam de paralelismo. Às vezes, transformar código iterativo em operações paralelas pode acrescentar complexidade sem ganho real.

5. Manter a legibilidade com funções utilitárias

Crie funções auxiliares para encapsular padrões comuns com Promise.all, facilitando a reutilização e a leitura do código.

Erros comuns ao usar Promise.all e como evitá-los

  • Não manter a ordem: algumas visões erradas acreditam que a ordem é aleatória. Na verdade, a ordem dos resultados preserva a ordem de entrada. Confie na API e teste com dados simples para entender o mapeamento.
  • Promessas não retornadas: é comum esquecer que mapear um array de inputs para uma série de promessas cria promessas não resolvidas. Garanta que cada item leva a uma promessa válida.
  • Falhas silenciosas: se uma promessa falhar e você não tratar o erro, a aplicação pode entrar em estado inconsistente. Use try/catch ou .catch extensivamente.
  • Negligenciar timeouts: sem tempo limite, as operações podem ficar presas. Combine Promise.all com abortos para evitar gargalos.

Desempenho e concorrência: o que observar ao usar Promise.all

Paralelizar operações pode acelerar significativamente o tempo total de conclusão, especialmente quando cada tarefa envolve I/O, como chamadas HTTP, leitura de arquivos ou consultas a banco de dados. No entanto, há limites práticos:

  • Taxa de requisições: muitas chamadas simultâneas podem saturar o servidor ou o cliente.
  • Uso de recursos: cada promesa pode abrir sockets, leituras de disco ou conexões; isso consome CPU e memória.
  • Ordem de dependências: se as operações dependem de resultados de outras, a paralelização deve ser cuidadosamente orquestrada.

Uma prática comum é iniciar um número controlado de operações paralelas (por exemplo, um batching de 5 a 10 promessas por vez) para equilibrar velocidade e estabilidade, especialmente em aplicações móveis ou com conectividade variável.

Casos de uso reais de Promise.all

Sincronizar dados de múltiplas fontes

Apps que agregam dados de várias APIs para construir um dashboard costumam usar Promise.all para buscar dados de métricas, usuários, e itens de uma só vez, reduzindo a latência total percebida pelo usuário.

Carregar recursos de configuração antes da inicialização

Carregar configurações, temas, permissões e recursos de inicialização do app com Promise.all permite que o usuário veja a interface mais cedo, com dados de configuração já prontos para uso.

Processar lotes de registros de banco de dados

Em operações de ETL ou sincronização, várias consultas ao banco podem ser executadas em paralelo, tornando o tempo total de extração e transformação mais rápido, sem comprometer a ordem de processamento.

Promise.all e reversões de ideias: explorando variações no texto

Ao escrever código ou documentar funcionalidades, muitas vezes é útil varrer o vocabulário para enfatizar o conceito. Além de usar o termo padrão Promise.all, é comum encontrar variações que reforçam o significado, incluindo a grafia promise.all em textos descritivos ou em títulos que desejam enfatizar o aspecto de API estática. Em código, a grafia com “Promise” com P maiúsculo é a correta, pois corresponde ao construtor global. Em textos informais, pode-se mencionar “all as promessas” para reforçar a ideia de que todas as promessas foram consideradas, mantendo a clareza para leitores que estão começando.

Perguntas frequentes sobre Promise.all

Promise.all funciona com qualquer iterável?

Sim, Promise.all aceita qualquer iterável de promessas ou de valores que podem ser convertidos para promessas. O mais comum é passar um array.

O que acontece se a entrada estiver vazia?

Se o input for um iterável vazio, Promise.all resolve imediatamente com um array vazio. Não há promessas para aguardar.

É possível cancelar Promise.all?

Promessa em si não tem cancelamento. Para controlar cancelamentos, combine Promise.all com AbortController ou estruturas similares, abortando as operações individuais antes que elas completem.

Como lidar com combinações de Promise.all com promessas que podem falhar?

Quando as promessas podem falhar, considere usar Promise.allSettled para obter todos os resultados e tratar falhas de forma granular, sem abortar o restante.

Boas práticas de código com Promise.all

  • Use Promise.all para operações independentes que precisam ocorrer ao mesmo tempo e quando o conjunto completo é essencial para a continuação.
  • Para cenários onde pelo menos um resultado é aceitável, mas não todos, prefira Promise.allSettled para observar cada resultado.
  • Combine com AbortController para cancelamento de várias operações em caso de timeout ou mudança de contexto.
  • Considere a divisão de cargas grandes em lotes menores para evitar saturar o servidor ou o cliente.
  • Documente as dependências entre as promessas para facilitar a manutenção e o onboarding de novos desenvolvedores.

Conclusão: o papel essencial de Promise.all no desenvolvimento moderno

Promise.all é uma ferramenta poderosa que, quando aplicada com cuidado, pode reduzir significativamente o tempo de carregamento e melhorar a experiência do usuário ao lidar com várias operações assíncronas. Ao entender não apenas como Promise.all funciona, mas também como ele se compara a outros métodos como Promise.allSettled, Promise.race e Promise.any, você ganha flexibilidade para escolher a estratégia mais adequada para cada cenário. Com boas práticas, tratamento de erros e atenção à performance, o uso de Promise.all se torna uma parte estável e eficiente de qualquer código moderno em JavaScript.