JavaScript: Armadilhas do Async/Await em loops

Entendendo e evitando comportamentos indesejádos

Eduardo Rabelo
4 min readApr 8, 2019

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 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!

Créditos ⭐️

--

--