Pourquoi votre async/await reste silencieux dans forEach ? Comprenons d'un premier coup d'œil
\n\nAvez-vous déjà écrit un code comme celui-ci, en espérant qu'il exécute les opérations asynchrones dans l'ordre ?
\n\nconst users = ['alice', 'bob', 'charlie'];\n\nusers.forEach(async (user) => {\n const data = await fetch(`https://api.example.com/users/${user}`);\n console.log(await data.json());\n});\n\nconsole.log('Terminé !');\n\nEt obtenu "Terminé !" avant même que les données ne soient chargées ? Cela vous dit quelque chose ? Vous n'êtes pas seul. C'est l'une des questions les plus populaires sur Stack Overflow avec des milliers de vues. Le problème est que forEach est une méthode synchrone. Elle exécute simplement les callbacks, mais n'attend pas leur achèvement. Vos fonctions async démarrent, mais forEach file déjà, comme un sprinteur qui se moque de l'arrivée des autres.
Dans cet article, nous ne montrerons pas seulement comment résoudre ce problème. Nous analyserons pourquoi cela se produit, et vous donnerons trois solutions fonctionnelles avec des exemples. Vous apprendrez quand utiliser Promise.all, et quand utiliser le bon vieux for...of. C'est parti !
Anatomie du problème : forEach et les promesses
\n\nRegardons sous le capot. La méthode forEach en JavaScript est conçue ainsi :
Array.prototype.forEach = function(callback) {\n for (let i = 0; i < this.length; i++) {\n callback(this[i], i, this);\n }\n};\n\nVous voyez ? Elle appelle simplement callback pour chaque élément, mais ne retourne rien. Même si le callback retourne une Promise, forEach l'ignore. Elle n'attend pas, ne collecte pas les résultats — elle exécute et oublie.
Lorsque vous écrivez async (user) => { ... }, votre fonction retourne une Promise. Mais forEach ne sait pas quoi en faire. Résultat : toutes les requêtes sont lancées en parallèle (s'il s'agit de fetch), mais vous ne pouvez pas attendre leur achèvement. Le code après forEach s'exécute instantanément.
Exemple concret : à quoi cela ressemble en pratique
\n\nImaginez que vous traitez un tableau d'ID de produits et que pour chacun, vous devez obtenir le prix depuis une base de données :
\n\nconst productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n // Simulation d'une requête à la BDD\n return new Promise(resolve => {\n setTimeout(() => {\n resolve(`Prix pour ${id}: $${Math.floor(Math.random() * 100)}`);\n }, 1000);\n });\n}\n\nproductIds.forEach(async (id) => {\n const price = await getPrice(id);\n console.log(price);\n});\n\nconsole.log('Tous les prix sont chargés !'); // S'exécute immédiatement, pas après 3 secondes\n\nRésultat dans la console :
\nTous les prix sont chargés !\nPrix pour 101: $42\nPrix pour 102: $78\nPrix pour 103: $15\n\nVous en conviendrez, ce n'est pas ce à quoi nous nous attendions. Bonne nouvelle : il existe des solutions simples et élégantes.
\n\nSolution n°1 : for...of — exécution séquentielle
\n\nLa façon la plus simple de faire fonctionner async/await dans une boucle est d'utiliser for...of. Contrairement à forEach, for...of fonctionne avec async/await nativement, car vous contrôlez le flux d'exécution à l'intérieur du bloc.
const productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n return new Promise(resolve => {\n setTimeout(() => {\n resolve(`Prix pour ${id}: $${Math.floor(Math.random() * 100)}`);\n }, 1000);\n });\n}\n\nasync function loadAllPrices() {\n for (const id of productIds) {\n const price = await getPrice(id);\n console.log(price);\n }\n console.log('Tous les prix sont chargés !');\n}\n\nloadAllPrices();\n\nMaintenant, le résultat sera :
\nPrix pour 101: $42\n(après 1 seconde)\nPrix pour 102: $78\n(après 1 seconde)\nPrix pour 103: $15\n(après 1 seconde)\nTous les prix sont chargés !\n\nAvantages : simplicité, lisibilité, ordre prévisible.
\nInconvénients : lent — chaque requête attend la fin de la p