Javascript : ce que vous ne devez pas faire avec async/await

Utiliser async/await c'est bien, sauf dans certains cas !

Article publié le 04/06/2021, dernière mise à jour le 19/09/2023

Avant de continuer : la lecture de cet article requiert la compréhension de ce que sont les mots-clés async et await en ES6 ainsi que leur utilisation.

Si ce n'est pas le cas, je vous invite à lire ce tutoriel (en anglais) sur le site javascript.info !

Pour rappel : async permet à une fonction à retourner une promesse, tandis que le mot-clé await permet d'attendre la résolution d'une promesse de manière synchrone (ou bloquante) et de récupérer la valeur retournée directement dans une variable.

En pratique, les promesses servent à éviter le callback-hell, et async/await permettent d'éviter d'empiler les then à tout va !

Mais le piège de cette technologie réside dans le fait que du code synchrone est beaucoup plus facile à lire pour un développeur, mais il peut aussi être bien moins performant que du code asynchrone, ce qui induit parfois une mauvaise utilisation.

Exemple

Prenons une fonction qui va simuler un appel asynchrone quelconque (API, BDD, etc...) grâce à un simple timeout d'une seconde :

function t(){
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      resolve();
    },1000);
  })
}

On pourra alors appeler cette fonction et attendre son retour de manière synchrone en utilisant await comme ceci :

...
console.time();
await t();
console.timeEnd();
...

La console nous indiquera que l'exécution s'est terminée en 1 seconde et quelques, ce qui correspond au temps d'exécution du script, incluant la résolution de la promesse et du blocage par await.

Jusqu'ici tout va bien.

Si vous n'êtes pas familier avec la méthode console.time(), je vous invite à lire mon article dédié aux méthodes de l'API console à connaitre.

Ce qu'il ne faut pas faire

Imaginons que nous ayons maintenant besoin de faire plusieurs appels à notre API, et d'attendre la fin de tous ces appels avant de poursuivre l'exécution de notre code.

On pourrait alors être tenté d'écrire un code comme celui-ci :

...
for(let i=0 ; i<25 ; i++){
  await t();
}
...

Et là, horreur, notre script au complet aura mis un peu plus de 25 secondes à s'exécuter ! Et oui, parce que même si notre machine (ou notre API) aurait été capable de gérer tous ces appels en parallèle, nous avons forcé notre script à attendre le retour de chaque promesse avant d'effectuer un nouvel appel.

Ce code-là, bien que facilement lisible et compréhensible par un développeur (car écrit de manière "synchrone") casse en réalité tous les avantages de Javascript qui est un langage basé sur un cycle d'évènements et asynchrone par nature !

Cette démonstration a été réalisée par un professionnel, à ne pas reproduire chez vous.

La solution

Il existe une méthode qui permet de résoudre facilement cette problématique et de garder à la fois la lisibilité du code, et les performance.

Cette méthode s'appelle Promise.all(...), elle prend en paramètre un tableau de promesses en attente de résolution et renverra elle-même une nouvelle promesse uniquement lorsque toutes les promesses contenues dans le tableau passé en paramètre seront résolues.

Voici l'équivalent de la boucle précédente dans l'exemple ci-dessous :

...
const promises = [];
for(let i=0 ; i<25 ; i++){
  promises.push(t()); //non-blocking execution
}
await Promise.all(promises);
...

Comme vous pouvez le voir, à chaque fois que l'on va faire un appel à la méthode t(), on va stocker la promesse retournée dans un tableau.

Tous les appels à la fonction vont donc pouvoir être fait en parallèle, mais le reste de notre code ne s'exécutera qu'une fois que toutes les promesses seront résolues, comme auparavant.

Résultat : l'exécution du script et des 25 appels prend 1.024s, soit 25 fois moins de temps qu'avec la solution précédente.

Évidemment dans un cas réel, le temps d'exécution final dépendra de la capacité du service asynchrone (ici remplace par le timeout) à traiter des requêtes concurrentes, mais les performances seront toujours incomparables en utilisant Promise.all !

Je vous invite à lire la documentation officielle de cette méthode sur la documentation du Mozilla Developer Network !

Par ailleurs Promise.all(...) renvoie une promesse contenant bien évidemment le tableau des données retournée par chaque promesse résolue, dans l'ordre exact des appels effectués, donc aucun problème pour récupérer toutes vos données !

Conclusion

Si vous faire des appels asynchrones dans une boucle et attendre la fin de tous les appels, utilisez Promise.all au lieu de rendre votre code complètement synchrone avec async/await !


Nadine Shaabana sur Unsplash

Vous avez terminé l'article ?

Commentaires (0)

pour laisser un commentaire

Aucun commentaire pour l'instant