Domine os combinadores de Promise do JavaScript (Promise.all, Promise.allSettled, Promise.race, Promise.any) para programação assíncrona eficiente e robusta em aplicações globais.
Combinadores de Promise do JavaScript: Padrões Assíncronos Avançados para Aplicações Globais
A programação assíncrona é um pilar do JavaScript moderno, especialmente ao construir aplicações web que interagem com APIs, bancos de dados ou realizam operações demoradas. As Promises do JavaScript fornecem uma abstração poderosa para gerenciar operações assíncronas, mas dominá-las requer a compreensão de padrões avançados. Este artigo aprofunda-se nos combinadores de Promise do JavaScript – Promise.all, Promise.allSettled, Promise.race e Promise.any – e como eles podem ser usados para criar fluxos de trabalho assíncronos eficientes e robustos, particularmente no contexto de aplicações globais com condições de rede e fontes de dados variáveis.
Entendendo Promises: Uma Rápida Revisão
Antes de mergulharmos nos combinadores, vamos revisar rapidamente as Promises. Uma Promise representa o resultado eventual de uma operação assíncrona. Ela pode estar em um de três estados:
- Pendente (Pending): O estado inicial, nem cumprida nem rejeitada.
- Cumprida (Fulfilled): A operação foi concluída com sucesso, com um valor resultante.
- Rejeitada (Rejected): A operação falhou, com um motivo (geralmente um objeto Error).
As Promises oferecem uma maneira mais limpa e gerenciável de lidar com operações assíncronas em comparação com os callbacks tradicionais. Elas melhoram a legibilidade do código e simplificam o tratamento de erros. Crucialmente, elas também formam a base para os combinadores de Promise que exploraremos.
Combinadores de Promise: Orquestrando Operações Assíncronas
Os combinadores de Promise são métodos estáticos no objeto Promise que permitem gerenciar e coordenar múltiplas Promises. Eles fornecem ferramentas poderosas para construir fluxos de trabalho assíncronos complexos. Vamos examinar cada um em detalhes.
Promise.all(): Executando Promises em Paralelo e Agregando Resultados
Promise.all() recebe um iterável (geralmente um array) de Promises como entrada e retorna uma única Promise. Esta Promise retornada é cumprida quando todas as Promises de entrada foram cumpridas. Se alguma das Promises de entrada for rejeitada, a Promise retornada rejeita imediatamente com o motivo da primeira Promise rejeitada.
Caso de Uso: Quando você precisa buscar dados de múltiplas APIs simultaneamente e processar os resultados combinados, Promise.all() é ideal. Por exemplo, imagine construir um painel que exibe informações meteorológicas de diferentes cidades ao redor do mundo. Os dados de cada cidade poderiam ser buscados por meio de uma chamada de API separada.
async function fetchWeatherData(city) {
try {
const response = await fetch(`https://api.example.com/weather?city=${city}`); // Substitua por um endpoint de API real
if (!response.ok) {
throw new Error(`Falha ao buscar dados meteorológicos para ${city}`);
}
return await response.json();
} catch (error) {
console.error(`Erro ao buscar dados meteorológicos para ${city}: ${error}`);
throw error; // Relance o erro para ser capturado pelo Promise.all
}
}
async function displayWeatherData() {
const cities = ['London', 'Tokyo', 'New York', 'Sydney'];
try {
const weatherDataPromises = cities.map(city => fetchWeatherData(city));
const weatherData = await Promise.all(weatherDataPromises);
weatherData.forEach((data, index) => {
console.log(`Clima em ${cities[index]}:`, data);
// Atualize a interface do usuário com os dados do tempo
});
} catch (error) {
console.error('Falha ao buscar dados meteorológicos para todas as cidades:', error);
// Exiba uma mensagem de erro para o usuário
}
}
displayWeatherData();
Considerações para Aplicações Globais:
- Latência de Rede: Requisições para diferentes APIs em diferentes localizações geográficas podem experimentar latências variadas.
Promise.all()não garante a ordem em que as Promises são cumpridas, apenas que todas elas sejam cumpridas (ou uma seja rejeitada) antes que a Promise combinada seja resolvida. - Limitação de Taxa de API (Rate Limiting): Se você está fazendo múltiplas requisições para a mesma API ou múltiplas APIs com limites de taxa compartilhados, você pode exceder esses limites. Implemente estratégias como enfileirar requisições ou usar backoff exponencial para lidar com a limitação de taxa de forma elegante.
- Tratamento de Erros: Lembre-se que se qualquer Promise for rejeitada, toda a operação
Promise.all()falha. Isso pode não ser desejável se você quiser exibir dados parciais mesmo que algumas requisições falhem. Considere usarPromise.allSettled()em tais casos (explicado abaixo).
Promise.allSettled(): Lidando com Sucesso e Falha Individualmente
Promise.allSettled() é semelhante ao Promise.all(), mas com uma diferença crucial: ele espera que todas as Promises de entrada sejam resolvidas, independentemente de serem cumpridas ou rejeitadas. A Promise retornada sempre é cumprida com um array de objetos, cada um descrevendo o resultado da Promise de entrada correspondente. Cada objeto tem uma propriedade status (seja "fulfilled" ou "rejected") e uma propriedade value (se cumprida) ou reason (se rejeitada).
Caso de Uso: Quando você precisa coletar resultados de múltiplas operações assíncronas, e é aceitável que algumas falhem sem que a operação inteira falhe, Promise.allSettled() é a melhor escolha. Imagine um sistema que processa pagamentos através de múltiplos gateways de pagamento. Você pode querer tentar todos os pagamentos e registrar quais tiveram sucesso e quais falharam.
async function processPayment(paymentGateway, amount) {
try {
const response = await paymentGateway.process(amount); // Substitua por uma integração de gateway de pagamento real
if (response.status === 'success') {
return { status: 'fulfilled', value: `Pagamento processado com sucesso via ${paymentGateway.name}` };
} else {
throw new Error(`Pagamento falhou via ${paymentGateway.name}: ${response.message}`);
}
} catch (error) {
return { status: 'rejected', reason: `Pagamento falhou via ${paymentGateway.name}: ${error.message}` };
}
}
async function processMultiplePayments(paymentGateways, amount) {
const paymentPromises = paymentGateways.map(gateway => processPayment(gateway, amount));
const results = await Promise.allSettled(paymentPromises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
// Analise os resultados para determinar o sucesso/falha geral
const successfulPayments = results.filter(result => result.status === 'fulfilled').length;
const failedPayments = results.filter(result => result.status === 'rejected').length;
console.log(`Pagamentos bem-sucedidos: ${successfulPayments}`);
console.log(`Pagamentos falhos: ${failedPayments}`);
}
// Gateways de pagamento de exemplo
const paymentGateways = [
{ name: 'PayPal', process: (amount) => Promise.resolve({ status: 'success', message: 'Pagamento bem-sucedido' }) },
{ name: 'Stripe', process: (amount) => Promise.reject({ status: 'error', message: 'Fundos insuficientes' }) },
{ name: 'Worldpay', process: (amount) => Promise.resolve({ status: 'success', message: 'Pagamento bem-sucedido' }) },
];
processMultiplePayments(paymentGateways, 100);
Considerações para Aplicações Globais:
- Robustez:
Promise.allSettled()aumenta a robustez de suas aplicações, garantindo que todas as operações assíncronas sejam tentadas, mesmo que algumas falhem. Isso é particularmente importante em sistemas distribuídos onde as falhas são comuns. - Relatórios Detalhados: O array de resultados fornece informações detalhadas sobre o resultado de cada operação, permitindo que você registre erros, tente novamente operações falhas ou forneça feedback específico aos usuários.
- Sucesso Parcial: Você pode determinar facilmente a taxa de sucesso geral e tomar as medidas apropriadas com base no número de operações bem-sucedidas e falhas. Por exemplo, você pode oferecer métodos de pagamento alternativos se o gateway principal falhar.
Promise.race(): Escolhendo o Resultado Mais Rápido
Promise.race() também recebe um iterável de Promises como entrada e retorna uma única Promise. No entanto, ao contrário de Promise.all() e Promise.allSettled(), Promise.race() é resolvido assim que qualquer uma das Promises de entrada é resolvida (seja cumprida ou rejeitada). A Promise retornada é cumprida ou rejeitada com o valor ou motivo da primeira Promise resolvida.
Caso de Uso: Quando você precisa selecionar a resposta mais rápida de múltiplas fontes, Promise.race() é uma boa escolha. Imagine consultar múltiplos servidores para os mesmos dados e usar a primeira resposta que você receber. Isso pode melhorar o desempenho e a capacidade de resposta, especialmente em situações onde alguns servidores podem estar temporariamente indisponíveis ou mais lentos que outros.
async function fetchDataFromServer(serverURL) {
try {
const response = await fetch(serverURL, {signal: AbortSignal.timeout(5000)}); //Adiciona um timeout de 5 segundos
if (!response.ok) {
throw new Error(`Falha ao buscar dados de ${serverURL}`);
}
return await response.json();
} catch (error) {
console.error(`Erro ao buscar dados de ${serverURL}: ${error}`);
throw error;
}
}
async function getFastestResponse() {
const serverURLs = [
'https://server1.example.com/data', // Substitua por URLs de servidores reais
'https://server2.example.com/data',
'https://server3.example.com/data',
];
try {
const dataPromises = serverURLs.map(serverURL => fetchDataFromServer(serverURL));
const fastestData = await Promise.race(dataPromises);
console.log('Dados mais rápidos recebidos:', fastestData);
// Use os dados mais rápidos
} catch (error) {
console.error('Falha ao obter dados de qualquer servidor:', error);
// Trate o erro
}
}
getFastestResponse();
Considerações para Aplicações Globais:
- Timeouts: É crucial implementar timeouts ao usar
Promise.race()para evitar que a Promise retornada espere indefinidamente se algumas das Promises de entrada nunca forem resolvidas. O exemplo acima usaAbortSignal.timeout()para conseguir isso. - Condições de Rede: O servidor mais rápido pode variar dependendo da localização geográfica do usuário e das condições da rede. Considere usar uma Rede de Distribuição de Conteúdo (CDN) para distribuir seu conteúdo e melhorar o desempenho para usuários em todo o mundo.
- Tratamento de Erros: Se a Promise que 'vence' a corrida for rejeitada, então todo o Promise.race é rejeitado. Garanta que cada Promise tenha um tratamento de erros apropriado para evitar rejeições inesperadas. Além disso, se a promise "vencedora" for rejeitada devido a um timeout (como mostrado acima), as outras promises continuarão a ser executadas em segundo plano. Você pode precisar adicionar lógica para cancelar essas outras promises usando
AbortControllerse elas não forem mais necessárias.
Promise.any(): Aceitando a Primeira Promise Cumprida
Promise.any() é semelhante ao Promise.race(), mas com um comportamento ligeiramente diferente. Ele espera pela primeira Promise de entrada ser cumprida. Se todas as Promises de entrada forem rejeitadas, Promise.any() rejeita com um AggregateError contendo um array dos motivos da rejeição.
Caso de Uso: Quando você precisa obter dados de múltiplas fontes, e só se importa com o primeiro resultado bem-sucedido, Promise.any() é uma boa escolha. Isso é útil quando você tem fontes de dados redundantes ou APIs alternativas que fornecem a mesma informação. Ele prioriza o sucesso em vez da velocidade, pois espera pela primeira a ser cumprida, mesmo que algumas Promises sejam rejeitadas rapidamente.
async function fetchDataFromSource(sourceURL) {
try {
const response = await fetch(sourceURL);
if (!response.ok) {
throw new Error(`Falha ao buscar dados de ${sourceURL}`);
}
return await response.json();
} catch (error) {
console.error(`Erro ao buscar dados de ${sourceURL}: ${error}`);
throw error;
}
}
async function getFirstSuccessfulData() {
const dataSources = [
'https://source1.example.com/data', // Substitua por URLs de fontes de dados reais
'https://source2.example.com/data',
'https://source3.example.com/data',
];
try {
const dataPromises = dataSources.map(sourceURL => fetchDataFromSource(sourceURL));
const data = await Promise.any(dataPromises);
console.log('Primeiros dados bem-sucedidos recebidos:', data);
// Use os dados bem-sucedidos
} catch (error) {
if (error instanceof AggregateError) {
console.error('Falha ao obter dados de qualquer fonte:', error.errors);
// Trate o erro
} else {
console.error('Ocorreu um erro inesperado:', error);
}
}
}
getFirstSuccessfulData();
Considerações para Aplicações Globais:
- Redundância:
Promise.any()é particularmente útil ao lidar com fontes de dados redundantes que fornecem informações semelhantes. Se uma fonte estiver indisponível ou lenta, você pode contar com as outras para fornecer os dados. - Tratamento de Erros: Certifique-se de tratar o
AggregateErrorque é lançado quando todas as Promises de entrada são rejeitadas. Este erro contém um array dos motivos de rejeição individuais, permitindo que você depure e diagnostique os problemas. - Priorização: A ordem em que você fornece as Promises para
Promise.any()importa. Coloque as fontes de dados mais confiáveis ou rápidas primeiro para aumentar a probabilidade de um resultado bem-sucedido.
Escolhendo o Combinador Certo: Um Resumo
Aqui está um resumo rápido para ajudá-lo a escolher o combinador de Promise apropriado para suas necessidades:
- Promise.all(): Use quando precisar que todas as Promises sejam cumpridas com sucesso, e você quer falhar imediatamente se alguma Promise for rejeitada.
- Promise.allSettled(): Use quando quiser esperar que todas as Promises sejam resolvidas, independentemente do sucesso ou falha, e você precisa de informações detalhadas sobre cada resultado.
- Promise.race(): Use quando quiser escolher o resultado mais rápido de múltiplas Promises, e você só se importa com a primeira que for resolvida.
- Promise.any(): Use quando quiser aceitar o primeiro resultado bem-sucedido de múltiplas Promises, e não se importa se algumas Promises forem rejeitadas.
Padrões Avançados e Melhores Práticas
Além do uso básico dos combinadores de Promise, existem vários padrões avançados e melhores práticas a serem lembrados:
Limitando a Concorrência
Ao lidar com um grande número de Promises, executá-las todas em paralelo pode sobrecarregar seu sistema ou exceder os limites de taxa da API. Você pode limitar a concorrência usando técnicas como:
- Divisão em blocos (Chunking): Divida as Promises em blocos menores e processe cada bloco sequencialmente.
- Usando um Semáforo: Implemente um semáforo para controlar o número de operações concorrentes.
Aqui está um exemplo usando divisão em blocos:
async function processInChunks(promises, chunkSize) {
const results = [];
for (let i = 0; i < promises.length; i += chunkSize) {
const chunk = promises.slice(i, i + chunkSize);
const chunkResults = await Promise.all(chunk);
results.push(...chunkResults);
}
return results;
}
// Exemplo de uso
const myPromises = [...Array(100)].map((_, i) => Promise.resolve(i)); //Cria 100 promises
processInChunks(myPromises, 10) // Processa 10 promises por vez
.then(results => console.log('Todas as promises resolvidas:', results));
Tratando Erros com Elegância
O tratamento de erros adequado é crucial ao trabalhar com Promises. Use blocos try...catch para capturar erros que possam ocorrer durante operações assíncronas. Considere usar bibliotecas como p-retry ou retry para tentar novamente operações falhas automaticamente.
async function fetchDataWithRetry(url, retries = 3) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Erro HTTP! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (retries > 0) {
console.log(`Tentando novamente em 1 segundo... (Tentativas restantes: ${retries})`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Espera 1 segundo
return fetchDataWithRetry(url, retries - 1);
} else {
console.error('Máximo de tentativas atingido. Operação falhou.');
throw error;
}
}
}
Usando Async/Await
async e await fornecem uma maneira com aparência mais síncrona de trabalhar com Promises. Eles podem melhorar significativamente a legibilidade e a manutenibilidade do código.
Lembre-se de usar blocos try...catch em torno das expressões await para lidar com erros potenciais.
Cancelamento
Em alguns cenários, você pode precisar cancelar Promises pendentes, especialmente ao lidar com operações de longa duração ou ações iniciadas pelo usuário. Você pode usar a API AbortController para sinalizar que uma Promise deve ser cancelada.
const controller = new AbortController();
const signal = controller.signal;
async function fetchDataWithCancellation(url) {
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`Erro HTTP! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch abortado');
} else {
console.error('Erro ao buscar dados:', error);
}
throw error;
}
}
fetchDataWithCancellation('https://api.example.com/data')
.then(data => console.log('Dados recebidos:', data))
.catch(error => console.error('Fetch falhou:', error));
// Cancela a operação de fetch após 5 segundos
setTimeout(() => {
controller.abort();
}, 5000);
Conclusão
Os combinadores de Promise do JavaScript são ferramentas poderosas para construir aplicações assíncronas robustas e eficientes. Ao entender as nuances de Promise.all, Promise.allSettled, Promise.race e Promise.any, você pode orquestrar fluxos de trabalho assíncronos complexos, tratar erros com elegância e otimizar o desempenho. Ao desenvolver aplicações globais, considerar a latência da rede, os limites de taxa da API e a confiabilidade da fonte de dados é crucial. Ao aplicar os padrões e as melhores práticas discutidas neste artigo, você pode criar aplicações JavaScript que são tanto performáticas quanto resilientes, oferecendo uma experiência de usuário superior para usuários em todo o mundo.