Skip links

Simplifiez vos promesses avec async/await : quatre exemples

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. Les fonctions asynchrones 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. Contrairement au passage des callbacks aux promesses, les fonctions asynchrones ne sont pas un changement de paradigme et peuvent être adoptées de manière progressive ou partielle.

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.

Erreurs synchrones et asynchrones

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