Otimizando a chamada de múltiplas promises [ Javascript ]
Tags: javascript, node, performance, algorithms
Published: 2022-09-11 18:45:17


Fala povo bonito 😀 
Hoje estou passando aqui para trazer uma dica rápida sobre promises que muita gente ainda desconhece,principalmente aqueles que estão conhecendo o ambiente Javascript/NodeJs, sobre como otimizar a execução de várias chamadas assíncronas (Promises) no ***javascript***. Bora conferir essa dica 🙃?

# async/await dentro de um lopp = bad news
Para começar vamos definir de forma bem sucinta o que é uma Promise. 
`Promises são ações que irão se completar em algum ponto futuro no tempo. Ou seja, são funções cujo valor você não conhece de imediato e ele só estará disponível após um intervalo x de tempo. No javascript uma função é caracterizada como promise quando possui o modificador async antes do nome. Uma promise pode ser resolvida( quando ocorre tudo certo na chamada da função) ou rejeitada ( quando ocorre algum erro na chamada da função )`.
Segue uma definição mais detalhada do que é uma promise: [clique aqui](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)

No javascript, na maioria das vezes em que um dev precisa executar várias chamadas assíncronas sobre um array de valores ele vai, fatalmente, executar um ***for loop***, um ***for..in*** ou mesmo um ***for..of*** para iterar em cada valor do array, esperar a promise terminar, fazer o que tiver que fazer para só então passar para a próxima chamada. Traduzindo em código, imagine que temos uma função que faz o envio de um e-mail e que essa função é assíncrona (ou seja, gera uma promise quando chamada), fica algo mais ou menos asssim:
```
const sendMail = async (email) => { // lógica que envia um e-mail para o email informado como parâmetro };
const emails = [a, b, c, ...];
for (let i = 0; i < valores.length ; i++){
    await sendMail(emails[i]);
}
/// resto do seu código ...
```
Essa abordagem não está de todo errada, afinal de contas, toda abordagem que produz o resultado esperado é por definição uma abordagem certa. Porém ela possui um gravíssimo problema de performance. Vamos analisar rapidamente o código e ver o que está acontecendo.

Dentro do nosso loop estamos esperando a promise ser resolvida, ou seja, a cada iteração do loop o nosso código pára e fica ali esperando o retorno da chamada para a função **sendmail** que fizemos. Se cada chamada dessa função levar 3 segundos e tivermos um array com 5 emails vamos levar 15 segundos para enviar todos, agora imagina que temos um array com uma feedList de 1 milhão de emails para enviar... Só de curiosidade vou dizer quanto tempo vai levar, **833.3333333 HORAS!!!**. Impraticável.
Além disso, essa abordagem esconde um problema pior ainda, **que é o bloqueio do Event Loop**, esse erro tem potencial para fazer todo o seu servidor parar. Eu explico mais detalhadamente o que é o event loop e porque devemos nos preocupar com essa característica no nodeJs, caso queira conferir aqui vai o link: [clique aqui para acesssar o artigo](https://dev.to/r9n/entendendo-e-solucionando-o-bloqueio-do-event-loop-no-nodejs-parte-1-3nec)

## Melhorando o cenário

Ok ok, já sabemos que usar **async/await** dentro de um loop é uma má ideia. Porém nós ainda temos os nossos 1 milhão de email para enviar, como vamos fazer ? 
É aí que entra uma funcionalidade salvadora para nós. E ela se chama **Promise.all** !!

Segue a definição do site da mozila sobre esse método (traduzido): 
`O método Promise.all() recebe um iterável de promessas como entrada e retorna uma única Promessa que resolve para uma matriz dos resultados das promessas de entrada. Essa promessa retornada será cumprida quando todas as promessas de entrada forem cumpridas ou se o iterável de entrada não contiver promessas. Ele rejeita imediatamente após qualquer uma das promessas de entrada rejeitando ou não promessas lançando um erro, e irá rejeitar com esta primeira mensagem/erro de rejeição.`


Traduzindo, o método Promise.all recebe um array de promises e retorna uma única promise que só irá ser resolvida quando todas as promises do array que foi passado forem resolvidas.
 Mas ai você deve estar pensando, "Poxa grande porcaria, vai demorar do mesmo jeito 🤷‍♂️". Calma jovem padawan, eu explico.
 Acontece que esse método não irá esperar uma promise se resolver para executar a próxima, ele vai executá-las **concorrentemente**, resolvendo para um array de resultados quando todas as promises se completarem, ou, alguma delas incorrer em erro.
 Dessa forma, se você tiver um array com três promises que levam, respectivamente 2,4 e 5 segundos para executar, a sua chamada usando Promise.all vai levar no máximo 5 segundos, já com a abordagem utilizando **async/await** dentro de um loop, levaríamos 2 + 4 + 5 = 11 segundos.

Promise.all é realmente uma mão na roda 😀

Traduzindo em imagens as duas abordagens, temos:


![gráfico promises resolvidas de forma sequencial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/caoinwawtj4ky00fpb87.png)


![gráfico promises resolvidas de forma concorrente](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u4uyx5zr0qjpz8f2duxi.png)
fonte: própria autoria


# Teste prático
Ok ok Ronaldo, muito bonito na teoria, mas me mostra o código!!!
Para os mais céticos, fiz um pequeno código de teste que ilustra a situação acima descrita, ou seja, o envio massivo de e-mails.
Neste exemplo vamos utilizar um array com 25 emails, considerando que cada chamada da nossa função **sendMail** leva 3 segundos ( Eita provedor de e-mail lento, Jesus Cristo).
Esse é o nosso código, você pode testá-lo online. Sugiro que você mude os valores e compile o código para entender e sentir a diferença entre as abordagens
Para editar e executar o código, [clique aqui](https://playcode.io/959310/)

```
 const timeout = 3000; // tempo que as promises levarão para se resolver

 const userMails = ['teste1@gmail.com','teste2@gmail.com','teste3@gmail.com','teste4@gmail.com','teste5@gmail.com',
 'teste6@gmail.com','teste7@gmail.com','teste8@gmail.com','teste9@gmail.com','teste10@gmail.com',
 'teste11@gmail.com','teste12@gmail.com','teste13@gmail.com','teste14@gmail.com','teste15@gmail.com',
 'teste16@gmail.com','teste17@gmail.com','teste18@gmail.com','teste19@gmail.com','teste20@gmail.com',
'teste21@gmail.com','teste22@gmail.com','teste23@gmail.com','teste24@gmail.com','teste25@gmail.com'
 ]

const sendMail = (email) => {
 
  return new Promise((resolve, reject) => {
    console.log(`Enviando email para ${email}`);

    setTimeout(() => {
      console.log(`Email enviado para ${email}`);

      resolve(`ok`);
    }, timeout);
  });
};

async function enviandoEmailsComForLoop() {
 
  const start = Date.now();

  for (let i = 0; i < userMails.length; i++) {
    await sendMail(userMails[i]);
  }
  const end = Date.now();

  console.log("Tempo total gasto para enviar com loop for:  ", end - start);
}

async function enviandoEmailsComPromiseAll() {
  const start = Date.now();

  await Promise.all(userMails.map((email) => sendMail(email)));

  const end = Date.now();

  console.log("Tempo total gasto para enviar com promise.all:  ", end - start);
}

enviandoEmailsComForLoop();

enviandoEmailsComPromiseAll();
```
Vamos comparar os resultados em ambas as abordagens. 

Com o **async/wait** dentro de um loop temos o seguinte resultado:

`Enviando email para teste1@gmail.com`
`Email enviado para teste1@gmail.com`
`Enviando email para teste2@gmail.com`
`Email enviado para teste2@gmail.com`
`Enviando email para teste3@gmail.com`
`Email enviado para teste3@gmail.com`
`...(resumindo)`
`Email enviado para teste25@gmail.com`
```
Tempo total gasto para enviar com loop for:  75.228 segundos
```
Ou seja, gastou 75.228 segundos, pouco mais de 1 minuto

Utilizando o **promise.all** temos essa saída:

`Enviando email para teste1@gmail.com`
`Enviando email para teste1@gmail.com`
`... (resumindo)`
`Enviando email para teste25@gmail.com`
```
 Tempo total gasto para enviar com promise.all:  3.018 segundos
```
Ou seja, levou 3.018 segundos para enviar os 25 e-mails. Bem mais rápido 😀

# Nem tudo são flores...
Como aprendemos desde o início, nem tudo são flores e especialmente em computação não existe almoço de graça. Apesar de trazer um grande benefício, o uso desse método possui alguns contras. A saber:

 - Se qualquer uma das promises for rejeitada, a promise intera do método Pormise.all será rejeitada ( Veremos outro método que nos auxilia neste caso )
 - Nem todos os problemas podem ser resolvidos utilizando este método, por exemplo se você depende do resultado da promise 1 para chamar a promise 2 então não será possível utilizar esse método dado que ele resolve todas as promises para um array de resultados, não permitindo operar promises individualmente.
 - Ele retorna um array de results, o que pode ser mais chato de fazer o parse dependendo dos possíveis resultados das promises chamadas
 - Naturalmente ele vai consumir um pouquinho a mais de recursos para efetuar essas operações de forma **concorrente**.
 

# Algumas observações

1) Existe ainda o método **Promise.allSettled()** que funciona exatamente como o método **Promise.all()** porém com a vantagem de que, mesmo que qualquer uma das promises falhe ele vai continuar a execução das outras.

2) Existe ainda o método **Promise.any** que funciona exatamente como os métodos **Promise.all()** e **Promise.allSettled()**, porém diferentemente dos outros dois métodos, ele não irá esperar todas as promises se resolverem, tão logo qualquer uma das promises retorne algo, o método irá retornar o valor retornado pela promise que terminou primeiro.

3) 🛑**Importante**🛑!!! Não confunda **concorrente** com **paralelo**. O método **Promise.all** trabalha de forma concorrente. Ou seja, ele vai disparar as chamadas para o sistema interno do nodeJs e então a cada loop do EventLoop o V8(motor por debaixo do nodeJs) vai checando cada uma das promises para ver se está resolvida. Mas tenha em mente que isso é **concorrência** e não **paralelismo**. Paralelismo seria se todas elas fossem executadas uma em cada thread/Core no sistema, o que não é o caso aqui.

# Uma dica importante

Para evitar esse tipo de problema é aconselhável o uso de linters de código, hoje eles já possuem muitas regras mapeadas que impedem que esse tipo de situação aconteça, detectando e alertando o desenvolvedor assim que o código é escrito.
Essa situação que analisamos, por exemplo, está descrita em uma regra do EsLint, caso queira conferir, essa é a regra [Eslint Rule](https://eslint.org/docs/latest/rules/no-await-in-loop)

Caso você deseje saber mais sobre os outros métodos disponíveis, segue a documentação oficial da Mozila [clique aqui](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)

Bom é isso pessoal, essa foi a dica de hoje espero que esse conhecimento lhes seja útil.
Fiquem com Deus e até a próxima 🙂