JavaScript: Armadilhas do Async/Await em loops
Tags: javascript, node, webdev, ptbr
Published: 2019-04-08 10:33:50

## Entendendo e evitando comportamentos indesejádos

Usar o `async`/`await` ao criar loops em arrays no Javascript parece simples, mas há um comportamento não tão intuitivo a ser observado ao combinar os dois. Vamos dar uma olhada em três exemplos diferentes, para ver o que você deve prestar atenção e qual é o melhor para casos de uso específicos.

# forEach

Se você puder tirar apenas uma coisa deste artigo, que seja: `async`/`await` **não funciona em Array.prototype.forEach**. Vamos ver um exemplo para ver o porquê:

```
const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'
];

async function getTodos() {
  await urls.forEach(async (url, idx) => { 
    const todo = await fetch(url);
    console.log(`Received Todo ${idx+1}:`, todo);
  });
  
  console.log('Finished!');
}
```

Resultado:

```
Finished!
Received Todo 2, Response: { ··· }
Received Todo 1, Response: { ··· }
Received Todo 3, Response: { ··· }
```

⚠️ Problema 1:

O código acima será executado com sucesso. No entanto, observe que `Finished!` foi mostrado primeiro, apesar do uso de `await` antes do `urls.forEach`. O primeiro problema é que você não pode fazer `await` no loop inteiro ao usar `forEach`.

⚠️ Problema 2:

Além disso, apesar do uso de `await` dentro do loop, ele não esperou que cada solicitação terminasse antes de executar a próxima. Então, os pedidos foram registrados fora de ordem. Se a primeira solicitação demorar mais que as solicitações a seguir, ela ainda poderá terminar por último.

Por ambas as razões, `forEach` não deve ser invocado se você estiver usando `async`/`await`.

# Promise.all

Vamos resolver o problema de esperar que todo o loop seja concluído. Como `await` cria uma `Promise` sob o capô, podemos usar `Promise.all` com `await` para esperar todos os pedidos que foram iniciados durante o loop:

```
const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'
];

async function getTodos() {
  const promises = urls.map(async (url, idx) => 
    console.log(`Received Todo ${idx+1}:`, await fetch(url))
  );

  await Promise.all(promises);

  console.log('Finished!');
}
```

Resultado:

```
Received Todo 1, Response: { ··· }
Received Todo 2, Response: { ··· }
Received Todo 3, Response: { ··· }
Finished!
```

Resolvemos a questão de esperar que todas as solicitações terminem antes de continuar. Parece também que resolvemos a questão dos pedidos que acontecem fora de ordem, mas não *é exatamente esse* o caso.

Como mencionado anteriormente, `Promise.all` irá esperar a execução de todas as promessas feitas em paralelo. Não iremos esperar que o primeiro pedido seja retornado antes de começar o segundo, ou o terceiro. Para a maioria das finalidades, isso é bom e é uma solução muito eficaz. Mas, se você realmente precisar que cada solicitação aconteça em ordem , `Promise.all` **não resolverá isso**.

# for..of

Sabemos que `forEach` não respeita em nada o `async`/`await` e `Promise.all` só funciona se a ordem de execução não for importante. Vamos ver uma solução que resolve os dois casos.

O `for..of` executa o loop na ordem esperada - aguardando que cada operação `await` anterior seja concluída antes de passar para a próxima:

```
const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'
];

async function getTodos() {
  for (const [idx, url] of urls.entries()) {
    const todo = await fetch(url);
    console.log(`Received Todo ${idx+1}:`, todo);
  }

  console.log('Finished!');
}
```

Resultado:

```
Received Todo 1, Response: { ··· }
Received Todo 2, Response: { ··· }
Received Todo 3, Response: { ··· }
Finished!
```

Eu particularmente gosto de como esse método permite que o código permaneça linear - o que é um dos principais benefícios de usar `async`/`await`. Eu acho muito mais fácil de ler do que as alternativas.

Se você não precisa acessar o índice, o código fica ainda mais conciso:

```
for (const url of urls) { ··· }
```

Uma das principais desvantagens de usar um loop `for..of` é que sua performance é baixa em comparação com as outras opções de loop em JavaScript. No entanto, o argumento de desempenho é insignificante quando usado em chamadas `await` assíncronas, uma vez que a intenção é manter o loop até que cada chamada seja resolvida. Eu normalmente só uso for..of` se a ordem de execução assíncrona for importante.

**Nota:** Você também pode usar loops `for` básicos para obter todos os benefícios de `for..of`, mas eu gosto da simplicidade e legibilidade que `for..of` oferece.

---

👏 Se você achou este artigo útil e gostaria de ver mais, por favor, comente abaixo ou deixei algumas palmas! 🔗 Fique ligado para mais artigos como este!

---

**Atualização 21/08/2019:** Baseado no comentário do [Yukihiro Yamashita](https://medium.com/@yukihiroyamashita99) no [Medium](https://medium.com/@yukihiroyamashita99/quanto-as-fun%C3%A7%C3%B5es-recursivas-o-comportamento-%C3%A9-o-mesmo-do-for-each-146a954de01) sobre funções recursivas. Tomei a liberdade e criei um exemplo de como fazer um “fetch recursivo”, lembre-se de criar uma função exaustiva para evitar loop infinito!

- https://codesandbox.io/s/live-demo-recursive-fetch-7ijsc

# Créditos ⭐️

- [The Pitfalls of Async/Await in Array Loops](https://medium.com/@heytory), escrito originalmente por [Tory Walker](https://twitter.com/toryjwalker)