La version 8.9 de Node.js, sortie cet automne, est la première version LTS (
long time support) incluant le support de ECMAScript 2017. C’est l’occasion de simplifier l’usage des promesses grâce aux
fonctions asynchrones et aux mots-clés
async et
await : votre code sera plus simple, lisible et maintenable. En bref :
clean.
Node.js, comme souvent avec JavaScript, repose sur des opérations asynchrones pour gérer la concurrence, en particulier dans le cas des entrées/sorties. Historiquement ces opérations asynchrones étaient gérées à l’aide de
callbacks, ce qui demandait un effort très important de la part des développeurs pour éviter le fameux
callback hell :

Avec les
promesses, supportées par Node.js depuis la version 0.12, il est possible de grandement simplifier les opérations asynchrones, avec une remise à plat et une meilleure gestion des erreurs :
L’exemple est éloquent : l’utilisation de promesses simplifie le code et la gestion des erreurs n’est plus dupliquée. poussent le concept des promesses encore plus loin en ajoutant un sucre syntaxique qui permet de rendre le code encore plus concis et permet d’éviter au passage quelques travers des promesses. Démontrons-le avec quatre exemples.
Fonctions asynchrones
Une fonction asynchrone est définie à l’aide du mot-clé
async , avant une fonction normale (
async function m(args) { … } ) ou une fonction fléchée (
async (args) => … ), et permet d’utiliser l’opérateur
await . Cet opérateur s’utilise avant une expression. Si le résultat de l’évaluation de l’expression est une promesse, l’exécution de la fonction s’interrompt jusqu’à ce que la promesse soit terminée (on dit qu’elle est
settled , acquittée). Dans le cas où la promesse est résolue, l’opérateur
await retourne la valeur résolue, sinon la promesse est rejetée et une exception est lancée. Si le résultat de l’évaluation de l’expression de await n’est pas une promesse, il est simplement retourné de manière synchrone.
L’exemple précédent peut donc être réécrit de cette manière :
Ici, l’exécution de la fonction m est interrompu après l’appel à aPr , et reprend lorsque la promesse retournée par aPr a été résolue. La valeur résolue est ensuite assignée à resultFromA . On obtient ainsi un code qui exécute des fonctions asynchrones avec une syntaxe proche de celle utilisée pour les appels synchrones. Notons que les fonctions aPr , bPr , et cPr , sont les même que dans l’exemple précédent et retourne des promesses.
Une fonction asynchrone retourne elle même une promesse, qui est résolue lorsque l’exécution de la fonction est terminée, avec la valeur éventuellement retournée. À noter que l’opérateur
await est implicite dans le cas de
return , donc «
return await p; » est équivalent à «
return p; ».
Avantages des fonctions asynchrones par l’exemple
Un des principaux avantages des fonctions asynchrones est évident : le
code est plus
concis et plus
propre. Il n’y a plus d’enchaînement d’appels à
then et de déclarations de fonction. Mais il y a bien d’autres intérêts à utiliser les fonctions asynchrones et je vais ici en illustrer quatre.
Lorsqu’on utilise la syntaxe classique des promesses, il y a deux types d’erreur à gérer : les erreurs produites par les appels synchrones et les erreurs produites par les promesses. Avec
await , les promesses sont utilisées comme des appels synchrones, y compris pour la gestion des erreurs. Prenons le code suivant :

Pour attraper une exception possiblement levée par l’appel synchrone à
a , il faut utiliser un
try/catch . Pour attraper une exception possiblement levée par l’appel asynchrone à
bPr , il faut utiliser la méthode
catch . Avec une fonction asynchrone on n’utilise plus que
try/catch :
Appels en boucle
Dans le cas où des opérations asynchrones inter-dépendantes doivent être exécutées en boucle, les promesses obligent à faire des appels récursifs qui sont peu compréhensibles et très difficiles à maintenir :

Ici, l’enchaînement de promesses est implicite et cette fonction est bien trop complexe pour sa taille. Avec une fonction asynchrone la boucle devient explicite :

Ces fonctions sont bien équivalentes, mais il faut une bonne connaissance des promesses pour comprendre la première.
Attention, si vos opérations synchrones sont indépendantes, il vaut mieux créer les promesses simultanéments et utiliser
Promise.all afin des les exécuter en concurrence.
Valeurs intérmédiaires
Il est parfois nécessaire de garder les résultats de plusieurs promesses chaînées. Différentes solutions existent pour résoudre ce problème mais toutes ont leurs défauts. On peut par exemple stocker les valeurs intermédiaires dans des variables mutables de la
closure :

Plus propre, on peut aussi s’appuyer sur
Promise.all pour retourner plusieurs valeurs :

Avec les fonctions asynchrones, on s’évite toute gymnastique mentale :
Appels conditionnels
Il arrive que certaines opérations asynchrones soient conditionnelles, dans ce cas il n’est plus possible de les mettre à plat et on se retrouve avec des promesses imbriquées :

Cette imbrication rappelle les problèmes que posent l’utilisation de
callbacks. Avec les fonctions asynchrones, on peut réduire le niveau de profondeur de la fonction :
Conclusion
Les fonctions asynchrones, avec l’opérateur
await , s’appuient sur les promesses pour offrir une syntaxe plus claire. Le code est ainsi plus propre, ce qui est important pour les opérations asynchrones qui demandent toujours plus de réflexion.
Si vous utilisez déjà des promesses, le passage aux fonctions asynchrones se fait en douceur. N’hésitez donc pas à les utiliser pour vos futurs développements, voire à réécrire certaines portions de votre code lors de la maintenance, histoire de respecter la règle des boyscouts :
toujours laisser le code plus propre que lorsqu’on est arrivé.
Si vous souhaitez vous lancer dans une démarche de clean code, cet article pourrait également vous intéresser.
Software craftsmanship : l’art du code et de l’agilité technique en entreprise
Social media