JavaScript: Mais performance e estabilidade com Promise.all e Promise.allSettled

Silvério Vale • 17 de dezembro de 2023

Homem esperando no final da linha de chegada de corrida de carros.

Você já trabalhou com Promises no JavaScript? E já ouviu falar do método Promise.all? E do Promise.allSettled? Esses métodos podem te ajudar a alcançar maior performance e estabilidade na sua solução.

Mas o que é uma Promise?

O objeto Promise, nativo do JavaScript, é uma referência para um trecho de código assíncrono. Ele te permite aguardar a resolução desse trecho, bem como executar ações nos casos de sucesso e de falha.

São uma alternativa ao uso de callbacks e são muito bem acompanhados por um async / await. Mas isso é assunto para outro post... Aqui, vamos nos ater aos métodos all e allSettled - que são muito úteis para ganhar velocidade e estabilidade.

Ações sequenciais e paralelas

Quando desejamos executar mais de uma ação assíncrona (como chamadas a APIs), devemos nos perguntar: uma ação depende do retorno da outra? Em outras palavras: eu preciso de esperar o retorno da primeira ação para depois disparar a segunda? Se a resposta for "sim", você está em um cenário de ações sequenciais, e os métodos mencionados não fazem tanto sentido aí. Porém, muitas vezes a resposta é "não". Ou seja: As ações são independentes e podem ser executadas em paralelo.

A vantagem de paralelizar é óbvia: o tempo total deixará de ser a soma do tempo das ações e passará a ser a duração da mais demorada delas.

Para exemplificar: suponha que você tenha que fazer duas chamadas a uma API: Uma para buscar o enderço do usuário logado e outra para buscar a lista de filmes favoritos do mesmo usuário. A primeira demora em média 2 minutos para retornar e a segunda demora 3 minutos. Note que o endereço do usuário não afeta a lista de filmes favoritos e vice-versa. Neste cenário, se você realizar as duas chamadas sequencialmente, a duração total será de 5 minutos. Por outro lado, se você paralelizar, a duração total será de apenas 3 minutos.

Ok... Uma API que demore tantos minutos para retornar informações tão simples tem sérios problemas... Mas acho que o exemplo serviu para ilustrar a vantagem do paralelismo.

Promise.all: Mais performance

O Promise.all permite que você execute várias ações assíncronas em paralelo e que você aguarde pela resolução bem sucedida de todas elas (ou pelo primeiro erro encontrado).

Isso é particularmente útil quando você deseja paralelizar um conjunto de chamadas e esperar pela resolução de todas elas. É até possível fazer isso sem o Promise.all - mas é bem mais complexo e menos elegante.

Retomando o exemplo anterior, suponha que você esteja construindo a tela de perfil do cliente, que inclui o endereço e também os filmes favoritos. Você quer paralelizar a consulta para ganhar performance, mas não deseja exibir as informações aos poucos ao usuário, à medida em que forem retornando da API. Você quer aguardar o retorno de todos os dados e exibir a tela toda de uma vez. O Promise.all te permitiria fazer exatamente isso.

Exemplo de utilização:

const [addressResult, favMoviesResult] = await Promise.all([
  clientApi.getAddress(),
  clientApi.getFavMovies(),
]);

console.log({ addressResult, favMoviesResult });

Promise.allSettled: Mais estabilidade

Este é um irmão menos conhecido do Promise.all, mas costuma ser muito útil para trazer maior estabilidade para o sistema. Quando estávamos introduzindo o Promise.all, mencionamos que ele retorna assim que todas as Promises forem resolvidas com sucesso, ou assim que a primeira retornar com erro.

Esse "ou" merece destaque, uma vez que um erro em qualquer das ações paralelizadas interrompe a execução, porém pode ser que nem todas sejam essenciais para o funcionamento. Para exemplificar, retornemos novamente ao exemplo da tela de perfil: você quer buscar o endereço e os filmes favoritos do cliente, MAS você decide que os filmes favoritos não são uma informação essencial para a tela, e - caso a busca falhe - você prefere exibir a tela sem essa informação do que retornar um erro 500 ao usuário.

Nessa situação, o Promise.all cede lugar ao Promise.allSettled. Este segundo permite que você aguarde até que todas as ações sejam resolvidas, com sucesso ou erro. Para cada item resolvido, haverá um campo de status indicando se a resolução foi bem sucedida ("fulfilled") ou não ("rejected"). Em caso de sucesso, você poderá acessar o valor retornado no campo "value". No retorno com falha, o motivo pode ser encontrado no campo "reason". A partir daí, você pode iterar pelos itens retornados e decidir se você deseja seguir em frente ou não.

Com essa ferramenta, podemos paralelizar ações com diferentes graus de importância e evitar que falhas em funções secundárias quebrem o fluxo inteiro.

Exemplo de utilização:

const [addressSettledResult, favMoviesSettledResult, logSettledResult] =
  await Promise.allSettled([
    clientApi.getAddress(),
    clientApi.getFavMovies(),
    clientApi.logAccess(),
  ]);

if (addressSettledResult.status === "rejected") {
  throw new Error("Too important to move on! Crash!");
}

const addressResult = addressSettledResult.value;

if (
  favMoviesSettledResult.status === "rejected" ||
  logSettledResult.status === "rejected"
) {
  console.warn("Fav movies or api logs failed, but let's go on anyways...");
}

console.log({ addressResult });

Promise.all vs. Promise.allSettled

Após conhecermos o Promise.allSettled poderíamos ficar com a impressão de que ele é sempre melhor e mais completo do que o Promise.all, mas não é bem assim.

Na verdade, o allSettled só deve ser usado quando possuímos alguma ação secundária no grupo de ações paralelizadas. Caso contrário - se tivermos apenas ações fundamentais para a continuidade do fluxo - o Promise.all será superior, uma vez que ele irá retornar mais rápido em caso de falha: assim que o primeiro erro ocorrer.

Como sempre, cabe a você, dev, decidir qual ferramenta é mais adequada para cada situação.

Outros métodos: any e race

Os métodos da Promise mencionados são, certamente, os que eu mais uso no dia a dia. Porém, existem outros que podem ser úteis em cenários específicos:

  • any(): Recebe um conjunto de Promises e retorna a primeira bem sucedida. Só retorna erro se todas as Promises falharem;
  • race(): Retorna assim que a primeira Promise for resolvida (com sucesso ou erro).

Conclusão

Performance e estabilidade são dois atributos relevantes para quase qualquer sistema. Os métodos apresentados permitem trazer ganhos (muitas vezes expressivos) com certa facilidade e elegância. Espero que sejam úteis no seu dia a dia.



Referências

Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Promise.all: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Promise.allSettled: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled

© 2023 Cartas de um Dev. Todos os Direitos Reservados.