Promesses
Introduction
Ce chapitre est dédié aux promesses. Mais avant d’explorer ce concept, il est pertinent de comprendre pourquoi l’utiliser.
S’ensuivra une analyse détaillée d’une promesse : ses états, l’approche semblabe à du code synchrone et la gestion des erreurs. Puis, nous verrons comment créer une promesse de différentes façons, ce qui permettera de bien appréhender le concept. Enfin, nous explorerons l’intégration des promesses dans Node et dans les générateurs.
Callbacks vs promesses
L’approche traditionnelle avec Node est d’utiliser des callbacks. Ainsi, on ne bloque pas le serveur en attendant d’exécuter une tâche : celle-ci est lancée, et une fois terminée, elle appelle une fonction.
Cependant, les callbacks souffrent de plusieurs maux. Et le premier est le problème de l’imbrication : effectivement, en cas d’appels successifs à de nombreuses callbacks, le code devient rapidement illisible, et pire, dur à maintenir.
De surcroît, il faut gérer les erreurs à la main : d’ailleurs, si l’on oublie d’en traiter une, on fait face à de gros problèmes (une erreur non traitée est difficile à identifier pendant l’exécution du programme). Ce n’est pas tout : les exceptions donnent aussi quelques sueurs froides. Par exemple, il suffit d’en lancer une au sein d’une callback pour obtenir un système instable car personne ne peut la rattraper (try {} catch {}). En outre, Node ne supporte pas les exceptions non gérées. Sans oublier le fait qu’il est impossible de récupérer la pile d’erreurs complète ! Vraiment gênant pour le débogage.
Autre subtilité, il est nécessaire de faire très attention à la manière dont on appelle une callback. Synchrone ou asynchrone...
Notion de promesse
La promesse est un paradigme de programmation asynchrone qui résout avec style beaucoup de problèmes que l’on trouve en utilisant le passage de continuations (la convention dans Node).
Et concrètement ? Une promesse est un objet représentant une valeur qui sera disponible dans le futur.
Par exemple, avec readFile() qui est asynchrone, le contenu du fichier foo.txt ne sera disponible que plus tard :
var promise = readFile('foo.txt');
Il est possible d’accéder à la valeur d’une promesse en enregistrant une fonction callback avec la méthode .then(callback) qui sera appelée quand cette valeur sera disponible. Voici ce que cela donne pour afficher le résultat de promise :
promise.then(function (content) {
console.log('le contenu de foo.txt est', content);
});
Une promesse peut être rejetée, par exemple si la lecture du fichier a échoué. L’erreur associée à ce rejet peut être récupérée de la même façon qu’est récupérée la valeur, mais avec la méthode .catch(callback) :
promise.catch(function (error) {
console.error('la lecture a échoué car', error);
});
Les promesses sont tellement pratiques qu’elles seront incluses par défaut dans la prochaine version de JavaScript (ECMAScript 6) avec l’objet Promise et qu’elles sont utilisées dans la plupart des nouvelles API de HTML5.
Outre l’implémentation officielle, il existe un grand nombre de bibliothèques implémentant les promesses tout en respectant la spécification standard A+, ce qui leur permet d’être complètement intercompatibles.
A+ est un standard open source pour les promesses en JavaScript. Il explicite la terminologie commune et le cahier des charges pour quiconque souhaiterait faire une bibliothèque de promesses. Pour plus de détails sur ce qu’est A+, nous vous invitons à...
Création d’une promesse
1. À la main
La création d’une promesse est simple : il suffit de passer une fonction au constructeur et d’utiliser les fonctions resolve(value) et reject(reason) pour définir l’état de la promesse.
Par exemple, pour lire un fichier et créer une promesse manuellement, fs.readFile est placé à l’intérieur d’une variable, nommée promise ici. En cas d’erreur renvoyée par fs.readFile, la promesse est rejetée, sinon résolue.
var promise = new Promise(function (resolve, reject) {
fs.readFile('texte.md', function (error, content) {
if (error) {
reject(error);
} else {
resolve(content);
}
});
});
2. À partir d’une fonction Node
Bluebird, comme la plupart des implémentations pour Node, fournit une méthode pour convertir une méthode utilisant la convention Node (c’est-à-dire prenant en dernier argument une fonction de type callback(error, result)) en une fonction renvoyant une promesse.
On dit que cette méthode « promessifie »...
Intégration avec Node
S’il est possible et trivial de créer une fonction renvoyant des promesses à partir d’une fonction utilisant les callbacks Node, il est également possible de faire le contraire, c’est-à-dire d’utiliser des promesses et d’exposer une interface basée sur les callbacks.
L’intérêt de cette pratique est d’écrire du code manipulant les promesses mais exposant une interface de programmation compatible avec les conventions Node, ce qui peut être un véritable atout pour une bibliothèque.
Pour faire ceci, Bluebird propose la méthode nodeify(callback) qui transmet le résultat de la promesse courante à la callback.
var readFile = Bluebird.promisify(fs.readFile);
function readJsonFile(path, callback) {
return readFile(path).then(JSON.parse).nodeify(callback);
}
Dans cet exemple, en plus de retourner une promesse, la fonction appelle la callback, si elle est définie, avec le résultat de l’opération.
Intégration avec les générateurs
À partir de Node 0.12, il devient encore plus simple d’utiliser les promesses avec la notion de générateur (nous ne rentrerons pas dans le détail ici). Il suffit de comprendre que dans un générateur, le mot-clé yield permet de retourner une valeur intermédiaire et de suspendre temporairement l’exécution de la fonction.
Voici un exemple d’un générateur qui renvoie l’ensemble des entiers entre deux valeurs :
function *range(a, b) {
var a = Math.ceil(a);
while (a < b) {
yield a;
a += 1;
}
}
L’appel à un générateur renvoie un itérateur, c’est-à-dire un objet possédant (entre autres) la méthode next() qui permet de continuer l’exécution du générateur. Voici un exemple d’utilisation du générateur range écrit précédemment :
var iterator = range(0, 2);
// {next: Function}
iterator.next();
// {done: false, value: 0}
iterator.next();
// {done: false, value: 1}
iterator.next();
// {done: true}...