Concorrência Real em JavaScript: Coordenando Múltiplas Promises na Prática Já leu

O Que é Concorrência Real em JavaScript? Diferente da concorrência "falsa" criada pelo event loop, concorrência real envolve executar múltiplas operações assíncronas em paralelo, coordenando seus resultados de forma eficiente. Em JavaScript, isso é realizado através de Promises, que permitem trabalhar com operações não-bloqueantes como requisições HTTP, leitura de arquivos e acesso a bancos de dados. Uma Promise representa um valor que pode estar disponível agora, no futuro ou nunca. O grande desafio é coordenar várias Promises simultaneamente, esperando que todas sejam resolvidas ou tratando falhas parciais. Para dominar concorrência real, você precisa entender quando usar , , e — cada uma com seu caso de uso específico. Coordenando Múltiplas Promises com Métodos Nativos Promise.all() — Tudo ou Nada Use quando todos os resultados são críticos. Se uma Promise rejeita, toda a operação falha imediatamente: Promise.allSettled() — Resultados Parciais Aceitáveis Quando você precisa dos resultados de todas as Promises, mas algumas podem falhar: Arquivo ${index + 1}: Sucesso - Arquivo

O Que é Concorrência Real em JavaScript?

Diferente da concorrência "falsa" criada pelo event loop, concorrência real envolve executar múltiplas operações assíncronas em paralelo, coordenando seus resultados de forma eficiente. Em JavaScript, isso é realizado através de Promises, que permitem trabalhar com operações não-bloqueantes como requisições HTTP, leitura de arquivos e acesso a bancos de dados.

Uma Promise representa um valor que pode estar disponível agora, no futuro ou nunca. O grande desafio é coordenar várias Promises simultaneamente, esperando que todas sejam resolvidas ou tratando falhas parciais. Para dominar concorrência real, você precisa entender quando usar Promise.all(), Promise.race(), Promise.allSettled() e Promise.any() — cada uma com seu caso de uso específico.

Coordenando Múltiplas Promises com Métodos Nativos

Promise.all() — Tudo ou Nada

Use Promise.all() quando todos os resultados são críticos. Se uma Promise rejeita, toda a operação falha imediatamente:

const fetchUserData = async () => {
  const urls = [
    'https://api.github.com/users/torvalds',
    'https://api.github.com/users/gvanrossum',
    'https://api.github.com/users/brendaneich'
  ];

  try {
    const responses = await Promise.all(urls.map(url => fetch(url)));
    const data = await Promise.all(responses.map(r => r.json()));
    console.log('Todos os dados carregados:', data);
  } catch (error) {
    console.error('Erro ao buscar dados:', error.message);
  }
};

fetchUserData();

Promise.allSettled() — Resultados Parciais Aceitáveis

Quando você precisa dos resultados de todas as Promises, mas algumas podem falhar:

const processMultipleFiles = async () => {
  const filePromises = [
    fetch('/api/file1').then(r => r.json()),
    fetch('/api/file2').then(r => r.json()),
    fetch('/api/file3').then(r => r.json())
  ];

  const results = await Promise.allSettled(filePromises);

  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Arquivo ${index + 1}: Sucesso -`, result.value);
    } else {
      console.log(`Arquivo ${index + 1}: Erro -`, result.reason.message);
    }
  });
};

processMultipleFiles();

Promise.race() e Promise.any()

Promise.race() retorna assim que a primeira Promise é resolvida ou rejeitada — útil para implementar timeouts. Promise.any() retorna a primeira Promise que resolve com sucesso, ignorando rejeições até que todas falhem:

// Implementando timeout com race()
const fetchWithTimeout = (url, timeoutMs) => {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('Timeout')), timeoutMs)
    )
  ]);
};

// Promise.any() — ignore falhas parciais
const tryMultipleServers = async () => {
  const servers = [
    fetch('https://server1.com/api'),
    fetch('https://server2.com/api'),
    fetch('https://server3.com/api')
  ];

  try {
    const firstSuccess = await Promise.any(servers);
    const data = await firstSuccess.json();
    console.log('Primeiro servidor respondeu:', data);
  } catch (error) {
    console.error('Todos os servidores falharam');
  }
};

tryMultipleServers();

Padrões Avançados: Controle Fino da Concorrência

Executar Promises Sequencialmente com Dependências

Nem sempre você quer paralelismo total. Quando uma operação depende do resultado da anterior, use async/await com loops:

const fetchRelatedData = async () => {
  try {
    // Busca usuário
    const userResponse = await fetch('/api/user/123');
    const user = await userResponse.json();
    console.log('Usuário:', user.name);

    // Busca posts do usuário (depende do ID)
    const postsResponse = await fetch(`/api/users/${user.id}/posts`);
    const posts = await postsResponse.json();
    console.log('Posts:', posts.length);

    // Busca comentários (depende dos IDs dos posts)
    const comments = await Promise.all(
      posts.map(post => fetch(`/api/posts/${post.id}/comments`).then(r => r.json()))
    );
    console.log('Total de comentários:', comments.flat().length);
  } catch (error) {
    console.error('Erro na cadeia de requisições:', error);
  }
};

fetchRelatedData();

Pool de Concorrência — Limitar Paralelismo

Para não sobrecarregar a aplicação, controle quantas Promises rodam simultaneamente:

const concurrencyPool = async (promises, poolSize) => {
  const results = [];
  const executing = [];

  for (let promise of promises) {
    const exec = Promise.resolve(promise).then(
      (result) => {
        executing.splice(executing.indexOf(exec), 1);
        return result;
      }
    );

    results.push(exec);
    executing.push(exec);

    if (executing.length >= poolSize) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
};

// Uso: processar 100 requisições com máximo de 5 simultâneas
const urls = Array.from({ length: 100 }, (_, i) => `https://api.example.com/item/${i}`);
const promises = urls.map(url => fetch(url).then(r => r.json()));

concurrencyPool(promises, 5).then(results => {
  console.log('Todas as requisições completadas:', results.length);
});

Tratamento de Erros e Debugging

Erros em Promises precisam de cuidado especial em concorrência. Use .catch() granular ou try/catch com async/await:

const robustFetching = async () => {
  const tasks = [
    fetch('/api/critical').catch(e => {
      console.error('Critical endpoint falhou:', e);
      throw e; // Re-lança para Promise.all detectar
    }),
    fetch('/api/optional').catch(e => {
      console.warn('Optional endpoint falhou, continuando com valor padrão');
      return { data: null }; // Recuperação graciosa
    })
  ];

  try {
    const [critical, optional] = await Promise.all(tasks);
    const criticalData = await critical.json();
    const optionalData = await optional.json();

    console.log('Processamento bem-sucedido:', { criticalData, optionalData });
  } catch (error) {
    console.error('Falha fatal na operação concorrente:', error);
  }
};

robustFetching();

Conclusão

Dominar concorrência em JavaScript significa escolher a ferramenta certa para cada cenário: use Promise.all() quando todos os resultados são essenciais, Promise.allSettled() para tolerância a falhas, Promise.race() para race conditions e Promise.any() para redundância. Implemente pools de concorrência para não sobrecarregar recursos e sempre trate erros com precisão. A combinação de async/await com esses métodos nativos oferece controle fino sobre operações paralelas, transformando código assíncrono complexo em lógica clara e performática.

Referências


Artigos relacionados