Promise.all, Promise.race, Promise.allSettled

1. Introdução às Promises Combinadas

No desenvolvimento moderno com JavaScript, Node.js e React, lidar com operações assíncronas simultâneas é uma necessidade constante. Seja carregando dados de múltiplas APIs, processando uploads em lote ou implementando timeouts, gerenciar essas operações de forma eficiente é crucial para a experiência do usuário e o desempenho do sistema.

Os três métodos estáticos do objeto PromisePromise.all, Promise.race e Promise.allSettled — oferecem soluções elegantes para diferentes cenários:

  • Promise.all: Ideal quando você precisa que todas as promises sejam resolvidas com sucesso antes de prosseguir. Falha rapidamente se qualquer uma rejeitar.
  • Promise.race: Útil para cenários de "primeiro a vencer", como timeouts ou requisições concorrentes onde apenas o resultado mais rápido importa.
  • Promise.allSettled: Perfeito quando você precisa do resultado de todas as promises, independentemente de sucesso ou falha, sem rejeição prematura.

2. Promise.all: Aguardando Todas as Promises com Sucesso

Promise.all recebe um iterável de promises e retorna uma única promise que resolve quando todas as promises do iterável forem resolvidas. O valor resolvido é um array com os resultados na mesma ordem das promises originais.

Comportamento de Rejeição

Se qualquer promise no iterável rejeitar, Promise.all rejeita imediatamente com o motivo da primeira rejeição, ignorando todas as outras promises (mesmo que algumas ainda estejam pendentes).

Exemplo em Node.js: Carregar dados de múltiplas APIs

const fetch = require('node-fetch');

async function carregarDadosUsuario(id) {
  const urls = [
    `https://api.exemplo.com/users/${id}`,
    `https://api.exemplo.com/users/${id}/posts`,
    `https://api.exemplo.com/users/${id}/albums`
  ];

  try {
    const [usuario, posts, albums] = await Promise.all(
      urls.map(url => fetch(url).then(res => {
        if (!res.ok) throw new Error(`Erro HTTP: ${res.status}`);
        return res.json();
      }))
    );

    return { usuario, posts, albums };
  } catch (erro) {
    console.error(`Falha ao carregar dados do usuário ${id}:`, erro.message);
    throw erro;
  }
}

Exemplo em React: Dashboard com useEffect

import { useState, useEffect } from 'react';

function Dashboard() {
  const [dados, setDados] = useState(null);
  const [erro, setErro] = useState(null);
  const [carregando, setCarregando] = useState(true);

  useEffect(() => {
    async function carregarDashboard() {
      try {
        const [vendas, usuarios, metricas] = await Promise.all([
          fetch('/api/vendas').then(r => r.json()),
          fetch('/api/usuarios').then(r => r.json()),
          fetch('/api/metricas').then(r => r.json())
        ]);
        setDados({ vendas, usuarios, metricas });
      } catch (err) {
        setErro('Falha ao carregar dashboard. Tente novamente.');
      } finally {
        setCarregando(false);
      }
    }

    carregarDashboard();
  }, []);

  if (carregando) return <p>Carregando dashboard...</p>;
  if (erro) return <p className="erro">{erro}</p>;
  return <div>{/* Renderizar dados */}</div>;
}

3. Promise.race: A Primeira Promise a Finalizar Vence

Promise.race retorna uma promise que resolve ou rejeita assim que qualquer promise do iterável for resolvida ou rejeitada. O valor ou motivo da primeira promise concluída é propagado.

Caso de Uso: Timeouts em Requisições

function fetchComTimeout(url, timeoutMs = 5000) {
  const controle = new AbortController();
  const timeoutId = setTimeout(() => controle.abort(), timeoutMs);

  return Promise.race([
    fetch(url, { signal: controle.signal }).then(res => {
      clearTimeout(timeoutId);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    }),
    new Promise((_, reject) => {
      setTimeout(() => {
        clearTimeout(timeoutId);
        reject(new Error(`Timeout após ${timeoutMs}ms`));
      }, timeoutMs);
    })
  ]);
}

Exemplo em React: Feedback ao Usuário para Requisições Lentas

import { useState, useRef } from 'react';

function BuscaRapida() {
  const [resultado, setResultado] = useState(null);
  const [status, setStatus] = useState('idle');
  const controleRef = useRef(null);

  async function buscarDados(url) {
    const controle = new AbortController();
    controleRef.current = controle;

    const promiseLenta = fetch(url, { signal: controle.signal }).then(r => r.json());
    const promiseTimeout = new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Requisição demorando muito...')), 3000)
    );

    try {
      setStatus('loading');
      const dados = await Promise.race([promiseLenta, promiseTimeout]);
      setResultado(dados);
      setStatus('success');
    } catch (erro) {
      if (erro.message === 'Requisição demorando muito...') {
        setStatus('slow');
        // Aguarda a promise original para obter o resultado real
        const dadosReais = await promiseLenta;
        setResultado(dadosReais);
        setStatus('success');
      } else {
        setStatus('error');
      }
    }
  }

  return (
    <div>
      <button onClick={() => buscarDados('/api/dados')}>Buscar</button>
      {status === 'slow' && <p>Carregando mais do que o esperado...</p>}
    </div>
  );
}

4. Promise.allSettled: Resultados de Todas as Promises Independentemente

Promise.allSettled é o método mais tolerante: ele aguarda todas as promises serem concluídas (resolvidas ou rejeitadas) e nunca rejeita. Retorna um array de objetos com a estrutura:

{ status: 'fulfilled', value: resultado }
// ou
{ status: 'rejected', reason: erro }

Exemplo em Node.js: Processamento em Lote com Falhas Toleradas

async function processarLote(ids) {
  const promises = ids.map(id => 
    fetch(`https://api.exemplo.com/processar/${id}`)
      .then(res => {
        if (!res.ok) throw new Error(`Falha no item ${id}`);
        return res.json();
      })
  );

  const resultados = await Promise.allSettled(promises);

  const sucessos = resultados
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value);

  const falhas = resultados
    .filter(r => r.status === 'rejected')
    .map(r => ({ id: r.reason.message.match(/\d+/)[0], erro: r.reason }));

  console.log(`${sucessos.length} processados com sucesso, ${falhas.length} falhas`);

  return { sucessos, falhas };
}

Exemplo em React: Upload Múltiplo com Feedback Individual

import { useState } from 'react';

function MultiplosUploads() {
  const [statusUploads, setStatusUploads] = useState([]);

  async function enviarArquivos(arquivos) {
    const promises = arquivos.map((arquivo, index) =>
      fetch('/api/upload', {
        method: 'POST',
        body: new FormData().append('arquivo', arquivo)
      }).then(res => {
        if (!res.ok) throw new Error(`Falha ao enviar ${arquivo.name}`);
        return res.json();
      })
    );

    const resultados = await Promise.allSettled(promises);

    const novosStatus = resultados.map((resultado, index) => ({
      nome: arquivos[index].name,
      status: resultado.status === 'fulfilled' ? 'sucesso' : 'erro',
      mensagem: resultado.status === 'fulfilled' 
        ? 'Upload concluído' 
        : resultado.reason.message
    }));

    setStatusUploads(novosStatus);
  }

  return (
    <div>
      <input type="file" multiple onChange={e => enviarArquivos(e.target.files)} />
      <ul>
        {statusUploads.map((item, i) => (
          <li key={i} className={item.status}>
            {item.nome}: {item.mensagem}
          </li>
        ))}
      </ul>
    </div>
  );
}

5. Comparação e Padrões de Uso Avançados

Tabela Comparativa

Método Todas resolvem Alguma rejeita Tempo de execução
Promise.all Array com resultados Rejeita imediatamente Até a última promise
Promise.race Resultado da primeira Rejeita com a primeira Até a primeira promise
Promise.allSettled Array com status de todas Nunca rejeita Até a última promise

Combinação de Métodos

Um padrão poderoso é usar Promise.allSettled para coletar resultados e depois Promise.all para processar apenas os bem-sucedidos:

async function carregarComFallbacks(urls) {
  const resultados = await Promise.allSettled(
    urls.map(url => fetch(url).then(r => r.json()))
  );

  const dadosValidos = resultados
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value);

  if (dadosValidos.length === 0) {
    throw new Error('Nenhuma fonte de dados disponível');
  }

  // Processa apenas os dados válidos
  return await Promise.all(
    dadosValidos.map(dado => processarDado(dado))
  );
}

Performance: Promise.all vs Loops Sequenciais

Promise.all executa todas as promises em paralelo, sendo significativamente mais rápido que loops sequenciais (for...of com await). Use Promise.all quando as operações forem independentes. Use loops sequenciais quando cada operação depender do resultado anterior.

6. Aplicações Práticas em Projetos Reais

Node.js: Agregação de Microsserviços

async function montarPaginaProduto(produtoId) {
  const [detalhes, estoque, precos, avaliacoes] = await Promise.all([
    servicoProdutos.buscar(produtoId),
    servicoEstoque.verificar(produtoId),
    servicoPrecos.calcular(produtoId),
    servicoAvaliacoes.listar(produtoId)
  ]);

  return { detalhes, estoque, precos, avaliacoes };
}

React: Hook Customizado useParallelFetch

function useParallelFetch(urls) {
  const [dados, setDados] = useState([]);
  const [carregando, setCarregando] = useState(true);
  const [erros, setErros] = useState([]);

  useEffect(() => {
    async function fetchAll() {
      setCarregando(true);
      const resultados = await Promise.allSettled(
        urls.map(url => fetch(url).then(r => r.json()))
      );

      const dadosSucesso = [];
      const errosEncontrados = [];

      resultados.forEach((r, i) => {
        if (r.status === 'fulfilled') {
          dadosSucesso.push({ url: urls[i], dados: r.value });
        } else {
          errosEncontrados.push({ url: urls[i], erro: r.reason });
        }
      });

      setDados(dadosSucesso);
      setErros(errosEncontrados);
      setCarregando(false);
    }

    if (urls.length > 0) fetchAll();
  }, [urls]);

  return { dados, carregando, erros };
}

Node.js: Health Check com Timeout

async function verificarServicos(servicos) {
  const healthChecks = servicos.map(servico =>
    Promise.race([
      fetch(`${servico.url}/health`).then(r => r.json()),
      new Promise((_, reject) => 
        setTimeout(() => reject(new Error(`Timeout: ${servico.nome}`)), 2000)
      )
    ])
  );

  const resultados = await Promise.allSettled(healthChecks);

  return servicos.map((servico, index) => ({
    nome: servico.nome,
    status: resultados[index].status === 'fulfilled' ? 'OK' : 'FALHA',
    detalhes: resultados[index].value || resultados[index].reason.message
  }));
}

7. Boas Práticas e Armadilhas Comuns

Efeitos Colaterais em Promises

Promises começam a executar assim que são criadas. Combinadores como Promise.all não "iniciam" as promises — elas já estão rodando. Isso é importante para evitar efeitos colaterais inesperados:

// ❌ Ruim: promises já iniciadas antes do combinador
const prom1 = fetch('/api/1');
const prom2 = fetch('/api/2');
// ... muito código ...
const resultado = await Promise.all([prom1, prom2]); // Requisições já foram feitas

// ✅ Bom: inicialização próxima ao uso
const resultado = await Promise.all([
  fetch('/api/1'),
  fetch('/api/2')
]);

Memory Leaks

Promises não tratadas (especialmente aquelas que rejeitam) podem causar memory leaks. Sempre use try/catch ou .catch() para tratar rejeições. Em React, cancele requisições em useEffect cleanup:

useEffect(() => {
  const controle = new AbortController();

  fetch('/api/dados', { signal: controle.signal })
    .then(r => r.json())
    .then(setDados)
    .catch(err => {
      if (err.name !== 'AbortError') setErro(err);
    });

  return () => controle.abort();
}, []);

Tratamento de Erros Granular vs Genérico

Com Promise.all, um único try/catch captura a primeira rejeição. Com Promise.allSettled, você tem controle granular sobre cada falha:

// Promise.all: tratamento genérico
try {
  await Promise.all(promises);
} catch (erro) {
  // Não sabemos qual promise falhou
}

// Promise.allSettled: tratamento granular
const resultados = await Promise.allSettled(promises);
resultados.forEach((r, i) => {
  if (r.status === 'rejected') {
    console.error(`Promise ${i} falhou:`, r.reason);
  }
});

Testes com Jest

describe('Promise combinadas', () => {
  it('Promise.all resolve com todas as promises', async () => {
    const prom1 = Promise.resolve(1);
    const prom2 = Promise.resolve(2);
    const resultado = await Promise.all([prom1, prom2]);
    expect(resultado).toEqual([1, 2]);
  });

  it('Promise.all rejeita com a primeira falha', async () => {
    const prom1 = Promise.resolve(1);
    const prom2 = Promise.reject(new Error('Falha'));
    await expect(Promise.all([prom1, prom2])).rejects.toThrow('Falha');
  });

  it('Promise.allSettled nunca rejeita', async () => {
    const prom1 = Promise.resolve(1);
    const prom2 = Promise.reject('Erro');
    const resultado = await Promise.allSettled([prom1, prom2]);
    expect(resultado[0].status).toBe('fulfilled');
    expect(resultado[1].status).toBe('rejected');
  });

  it('Promise.race resolve com a primeira promise', async () => {
    const rapida = new Promise(resolve => setTimeout(() => resolve('rápida'), 10));
    const lenta = new Promise(resolve => setTimeout(() => resolve('lenta'), 100));
    const resultado = await Promise.race([rapida, lenta]);
    expect(resultado).toBe('rápida');
  });
});

Referências

completo com exemplos práticos e comparação entre os métodos.

8. Conclusão

Dominar Promise.all, Promise.race e Promise.allSettled é essencial para qualquer desenvolvedor JavaScript que trabalhe com operações assíncronas em Node.js ou React. Cada método atende a um cenário específico:

  • Promise.all — ideal quando todas as operações precisam ser bem-sucedidas para o fluxo continuar, como carregar dados essenciais de múltiplas fontes.
  • Promise.race — perfeito para implementar timeouts e obter a resposta mais rápida entre várias alternativas.
  • Promise.allSettled — a escolha certa quando você precisa do resultado de todas as operações, independentemente de sucesso ou falha, como em processamento em lote ou uploads múltiplos.

A escolha correta entre eles pode significar a diferença entre um código robusto e resiliente e um sistema propenso a falhas inesperadas. Lembre-se sempre de considerar o tratamento de erros, o gerenciamento de memória e os efeitos colaterais ao trabalhar com promises combinadas.

Com os exemplos e padrões apresentados neste artigo, você está preparado para aplicar esses métodos em projetos reais, desde agregação de microsserviços em Node.js até hooks customizados em React, sempre com as boas práticas de performance e manutenibilidade.