Promise

Le concept de Promise date des années 70. Les Promises sont parfois appelés Future ou Deferred.

En 2010, le développeur Kris Kowal inspire la communauté JavaScript en implémentant ce concept pour NodeJS via la librairie Q https://github.com/kriskowal/q. Les implémentations se sont ensuite démultipliées jusqu'à ce que les Promises deviennent un standard avec ES6.

Fonctionnement des Promises

Le fonctionnement d'une Promise est généralement le suivant :

1. Un appelant fait appel à une fonction qui procède à un traitement asynchrone mais retourne de façon synchrone un objet container (la Promise) à l'appelant.

2. A ce stade, la Promise est le plus souvent dans un état "pending".

3. L'appelant inscrit sur la Promise une "callback" de succès pour être informé quand le résultat est disponible (e.g. : promise.then(data => console.log(data)) et une callback d'erreur pour être informé de l'échec (e.g. : promise.catch(error => console.error(error)).

4. Quand la fonction appelée obtient le résultat (ou une erreur), elle notifie la Promise qui passe alors à un état "resolved" (ou "rejected").

5. La Promise déclenche alors toutes les fonctions de "callback" de succès (ou d'erreur) qui ont pu lui être transmises (via les méthodes .then et .catch).

How Promise works

Consommation d'une Promise

A titre d'exemple, nous allons utiliser la fonction fetch désormais standard qui vient déloger la poussiéreuse XMLHttpRequest.

Cette fonction a la particularité de retourner une Promise.

fetch('https://www.googleapis.com/books/v1/volumes?q=extreme%20programming')
.then(response => {
console.log(response.status);
});

Pour accéder au "body" de la response, il faut utiliser la méthode Response.json qui retourne une Promise également.

fetch('https://www.googleapis.com/books/v1/volumes?q=extreme%20programming')
.then(response => {
response.json()
.then(data => {
console.log(data.totalItems);
});
});

Malheureusement, pour le moment, les problèmes associés au "callbacks waterfall" persistent.

Chaînage de Promises

Pour éviter les "callbacks waterfall", il est possible de chainer les Promise. En effet, les méthodes Promise.then et Promise.catch retournent des Promises.

Chaînage synchrone

fetch('https://www.googleapis.com/books/v1/volumes?q=extreme%20programming')
.then(response => response.status)
.then(status => console.log(status);

La ligne 1 retourne une Promise contenant la Response (elle est donc de type Promise<Response>).

La ligne 2 crée à son tour une nouvelle Promise déduite de la première et contenant le "status" (elle est donc de type Promise<number>).

La ligne 3 consomme donc le résultat de la Promise précédente.

Chaînage asynchrone

Une autre propriété intéressante des Promise est que si la valeur retournée à l'une des étapes est une Promise, alors l'étape suivante ne sera appelée que quand la Promise sera "resolved" et elle recevra en paramètre le résultat de résolution.

fetch('https://www.googleapis.com/books/v1/volumes?q=extreme%20programming')
.then(response => response.json())
.then(data => console.log(totalItems);

Chaînage et gestion d'erreurs

Si une erreur se produit à n'importe quelle étape soit car :

  • la Promise initiale est "rejected",

  • l'une des étapes lève une exception,

  • ou l'une des étapes retourne une Promise "rejected,

alors toutes les étapes suivantes sont ignorées et la "callback" associée au premier "catch" de la chaîne (à partir de l'erreur) est appelé.

fetch('https://www.googleapis.com/books/v1/volumes?q=extreme%20programming')
.then(response => response.JSON())
.then(data => console.log(totalItems))
.catch(error => console.error(error));

Nous obtenons alors l'erreur response.JSON is not a function à la ligne 4 (car la méthode se nomme json et non JSON).

Création de Promises

Pour créer une Promise, il faut instancier la classe Promise tel qu'indiqué ci-dessous et appeler la fonction "resolve" avec la donnée de résolution en cas de succès ou la méthode "reject" avec l'objet d'erreur en cas d'échec.

const getCurrentCity = () => {
​
return new Promise((resolve, reject) => {
/* Simulating some async stuff... */
setTimeout(() => {
if (hasPermission !== true) {
reject(new Error('Permission denied!'));
return;
}
resolve('Lyon');
}, 1000);
});
​
};
​
getCurrentCity()
.then(city => getWeatherInfo(city))
.then(weatherInfo => console.log(weatherInfo.temperature))
.catch(error => console.error(error));

Une Promise ne peut être "resolved" ou "rejected" qu'une seule fois.

C'est le premier appel qui gagne et qui définit donc l'état final de la Promise, les appels suivants sont simplement ignorés.

Vivement le jour où les appels superflus de "resolve" et "reject" déclencheront des erreurs.

Limitation du Scope

Parmi d'autres limitations que nous aborderons plus tard, dans le dernier exemple, on peut remarquer l'indisponibilité de la variable city lors du console.log de l'étape finale (ligne 23).

Il existe bien sûr des solutions de contournement mais peu séduisantes. Il est préférable d'adopter directement l'approche Async / Await pour éviter ces problèmes.