Les promesses

La programmation asynchrone a pris une place prépondérante dans le développement d’application en JavaScript. La raison principale vient du fait que l’interpréteur JavaScript intégré à tous les navigateurs Web ne supporte pas le parallélisme : le code JavaScript dans une page Web s’exécute dans un thread unique. Cela implique qu’il n’est pas possible de réaliser du code bloquant ou des boucles infinies. Pour le développement d’application serveur en JavaScript avec Node.js, le problème reste le même puisque Node.js reprend le même principe d’exécution dans un thread unique. Habituellement en programmation, les appels au système de fichiers ou les appels réseaux sont des appels bloquants. En JavaScript, comme un appel bloquant pénalise l’ensemble de la page Web ou de l’application, ce type d’appels est remplacé par un appel asynchrone.

La première façon de réaliser des appels asynchrones en JavaScript est d’utiliser des méthodes de callback passées en paramètres de l’appel asynchrone. Par exemple, le code suivant permet depuis une page Web de récupérer des données JSON sur un serveur distant (appel réseau) :

let xhr = new XMLHttpRequest();
xhr.open("GET", "http://monsite.fr/data.json");
xhr.responseType = "json";
xhr.addEventListener("load", function() {
    let data = xhr.response;
    // ...
});
xhr.send();

Dans l’exemple, ci-dessus on définit une fonction de callback pour l’événement load qui sera appelée lorsque les données auront été récupérées sur le réseau.

Un autre exemple pour une implémentation avec le serveur Node.js :

let http = require('http');

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Bienvenue sur mon serveur');
}).listen(8080);

On passe une fonction de callback à la fonction createServer qui sera appelée pour chaque requête reçue.

L’inconvénient des fonctions de callback est qu’elles rendent le code difficile à lire et donc difficile à maintenir. ES6 a introduit le modèle des promesses (promises). L’objectif est de produire du code asynchrone plus simple et plus lisible sous la forme d’une chaîne d’appels à des fonctions.

La classe Promise

ES6 fournit la classe Promise qui attend en paramètre une fonction. Cette dernière prend deux paramètres : resolve et reject. resolve est la fonction à appeler quand la tache asynchrone est finie et reject quand la tache asynchrone a échoué ou ne peut pas être exécutée.

Nous pouvons facilement transformer un appel ajax avec XMLHttpRequest sous la forme d’une promesse :

const URL = "http://quelquepart.com/data.json";

let ajax = new Promise((resolve, reject) => {
  let request = new XMLHttpRequest();
  request.open("GET", URL);

  request.addEventListener("load", function(){
    if(request.status === 200) {
      resolve(request.responseText);
    } else {
      reject("Erreur du serveur : " + request.status);
    }
  }, false);

  request.addEventListener("error", function(){
    reject("La requête ajax a échoué");
  }, false);

  request.send();
});

La méthode resolve est appelée en passant en paramètre le résultat de la tache. La méthode reject est appelée en passant un message d’erreur.

Une promesse dispose de la méthode then qui accepte en paramètre une méthode de callback fournissant le traitement à réaliser à la fin de la tache, c’est-à-dire lorsque la méthode resolve sera appelée. La fonction callback recevra en paramètre l’objet passé en paramètre de la méthode resolve. Dans notre cas, il s’agira de la réponse de la requête.

ajax.then(response => console.log("Réponse du serveur :", response));

Si la tache échoue et que c’est la méthode reject qui est appelée, alors on peut utiliser la méthode catch pour préciser le traitement à appliquer lors de l’échec. Cette méthode attend une fonction callback qui recevra en paramètre le message passé en paramètre de la méthode reject.

ajax.then(response => console.log("Réponse du serveur :", response))
    .catch(msg => console.log("Message d'erreur :", msg));

Note

Il est également possible de passer une fonction callback dans le cas d’un appel à reject en second paramètre de la méthode then.

ajax.then(response => console.log("Réponse du serveur :", response,
          msg => console.log("Message d'erreur :", msg));

Le chaînage des appels

Une fonction callback passée en paramètre de la méthode then peut elle-même produire un résultat. Si le résultat n’est pas un objet de type Promise alors un objet de type Promise est créé pour simplement appeler la fonction resolve avec la valeur de retour. Ainsi, le modèle des promesses garantie que l’on peut chaîner les appels à la méthode then.

ajax.then(response => JSON.parse(response))
    .then(json => {
      if (json.id === undefined) {
        throw Error("il manque l'id");
      }
      return json;
    })
    .then(json => {
      console.log(json);
      return json;
    })
    .catch(msg => console.log("Erreur : ", msg));

Si une des méthodes callback passées aux méthodes then produit une exception, le chaînage des appels est interrompu et c’est la fonction passée à catch qui est exécutée.

L’intérêt des promesses est de fournir un modèle lisible et universel pour la prise en charge des appels asynchrones. S’il est assez simple de créer ses propres promesses, de plus en plus de frameworks et de bibliothèques JavaScript proposent des API qui sont directement basées sur le modèle des promesses.

Traitements parallèles

Le modèle des promesses permet facilement de mettre en place des traitements parallèles.

Ainsi, la fonction Promise.all prend en paramètre un tableau (ou un itérable) de promesses et retourne une promesse qui s’achève lorsque toutes les promesses seront achevées.

La fonction Promise.race prend en paramètre un tableau (ou un itérable) de promesses et retourne une promesse qui s’achève lorsque la première des promesses sera achevée.