JavaScript: Armadilhas do Async/Await em loops
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 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 ⭐️
- The Pitfalls of Async/Await in Array Loops, escrito originalmente por Tory Walker