Une Promise est un objet qui représente une valeur qui n'est pas encore disponible, mais le sera à un certain moment dans le futur.

De la même manière où les callbacks vont nous permettre de gérer l'exécution de code asynchrone, les promises en sont la manière la plus élégante.

Toutes les nouvelles librairies ou frameworks sont promises based de nos jours !

La compréhension de ce concept est primordiale pour être apte à comprendre et écrire du code élégant et robuste pour votre application.

Promise

On peut traduire le mot Promise par Promesse. A la manière d'une callback, on attend un résultat ou une erreur à partir de l'exécution d'une promise.

Par définition, une Promise fournit une alternative plus simple pour l'exécution, la composition et la gestion des opérations asynchrones par rapport à l'approche basée sur les callbacks.

Une Promise permet également de gérer les erreurs asynchrones en utilisant une approche similaire à un try catch synchrone.

Votre code asynchrone doit nécessairement déclencher l'un deux des paramètres pour signaler la fin de la promise.

const promise = function() {
	return new Promise((resolve, reject) => {
		//	code asynchrone
		resolve();
	});
}

promise()
	.then(() => {
		console.log("mon code est exécuté");
	})
	.catch((error) => {
		console.log("mon code a rencontré une erreur");
	})

Imaginez un train sur un chemin de fer, il peut choisir entre la voie régulière de son trajet ou une voie d'urgence pour s'arrêter immédiatement.

  • S'il n'y a aucun incident dans les wagons, alors il continuera la voie régulière jusqu'à sa prochaine destination.
  • S'il y a un incident dans les wagons, alors il prendra la voie d'urgence pour pouvoir s'arrêter immédiatement.

Avec les Promises, c'est la même chose !

On créée une promise, on lui donne en paramètre une callback qui contient deux autres paramètres : resolve et reject

  • resolve : une fonction permettant de signaler à la promise la fin du code asynchrone
  • reject : une fonction permettant de signaler à la promise une erreur d'interruption immédiate

Parlons peu, parlons code.

L'exemple minimal de l'utilisation d'une promise ressemblerait à :

var promiseSauvegarderUtilisateur = function () {
	console.log("Code synchrone");
	return new Promise(function(resolve, reject) {
		console.log("Code asynchrone !");
		//	Les paramètres resolve et reject sont des fonctions
		//	Imaginons qu'il y ait du code ici pour sauvegarder notre utilisateur
		resolve();
	});
}

//	On exécute la fonction qui renvoie de suite la promise
//	On a donc accès aux fonctions de la promise
//	Une promise à une fonction "then()" qui signfie "puis" pour enchaîner une nouvelle callback lors de sa réussite
promiseSauvegarderUtilisateur()
	.then(function() {
		console.log("La sauvegarde de l'utilisateur fait !")
	})

Console :

La fonction resolve permet de signaler à l'objet Promise, nouvellement créé dans le return de la fonction, que le traitement est terminé avec succès !

La principale API d'une promise est la méthode then qui enregistre une callback pour recevoir d'éventuelles valeurs ou la raison pour laquelle il est impossible de la finir (sans erreur donc).

Comme on vient de le voir, une Promise est un objet qui a un ensemble de fonctions que l'on peut utiliser pour enchaîner une autre fonction OU une autre Promise !

On peut prouver que la promise s'exécute en asynchrone en rajoutant un simple setTimeout qui va rajouter un délai d'attente avant d'exécuter resolve().

//	La même chose que précédement !
var promiseSauvegarderUtilisateur = function () {
	console.log("Code synchrone");
	return new Promise(function(resolve, reject) {
		console.log("Code asynchrone !");
		//	On rajoute un setTimeout pour simuler une attente avant d'exécuter resolve()
		setTimeout(() => {
			resolve();
		}, 2500);	//	2,5 secondes avant l'exécution de resolve
	});
}

promiseSauvegarderUtilisateur()
	.then(function() {
		console.log("La sauvegarde de l'utilisateur fait !")
	})

console.log("Je suis là pour prouver que c'est asynchrone");

Console :

Gestion d'une erreur

En reprenant notre image du train, si notre code a le moindre problème, une erreur devrait être déclenchée !

Généralement, cette erreur est déclenchée par la librairie ou fonction qui est utilisée dans la partie du code asynchrone.

Pour l'exemple, nous allons voir comment intercepter une erreur que nous déclenchons volontairement puis une erreur générée par une fonction dans le code asynchrone.

On va réutiliser l'exemple sur le principe de l'échec rapide pour faire une addition en vérifiant les types des deux nombres en paramètre d'une fonction.

Quand la promise déclenche une erreur

Vous allez devoir généralement vérifier la nature des paramètres que vous recevez dans une fonction afin de garantir la robustesse de votre code.

Pour pouvoir alerter l'objet promise dans votre fonction, vous pouvez exécuter la fonction reject en lui donnant un erreur.

Les autres développeurs qui utiliseront votre code seront enchantés d'avoir une erreur compréhensible.

N'oubliez pas de coder comme s'ils savaient où vous habitez 😛

const addition = function(nombre1, nombre2) {
  return new Promise((resolve, reject) => {
   if (
     // Si number1 et number2 ne sont pas des nombres
     !( typeof nombre1 === 'number' && !isNaN(nombre1) && typeof nombre2 === 'number' && !isNaN(nombre2) )
    )
   {
     reject(new TypeError(
       'addition(): Les deux arguments doivent être des nombres. Arguments: "' + nombre1 + '" et "' + nombre2 + '"'
     ))
   }
   else {
     resolve(nombre1 + nombre2);
   }
  });
}

addition(4, 8)
	.then((result) => {
		console.log("Le résultat de 4 + 8 est", result);
	})
	.catch((error) => {
		console.log("Ooops", error);
	})

addition(5, "9")
	.then((result) => {
		console.log("Le résultat de 5 + 9 est", result);
	})
	.catch((error) => {
		console.log("Ooops", error);
	})

Console :

Quand une autre fonction déclenche une erreur

Imaginons que vous utilisez le code d'une librairie ou de votre team de développeurs.

Vous pouvez tout simplement enrober la fonction que vous utilisez dans votre promise avec un simple try/catch pour récupérer l'erreur.

Encore une fois, on utilise reject et resolve dans les deux cas possibles d'une promise.

const promiseAddition = function(nombre1, nombre2) {
	return new Promise((resolve, reject) => {
	    // Le try/catch va anticiper l'arrivée de l'erreur !
	    try {
	      // addition est une fonction qui se trouve ailleurs dans votre code, faisons-lui confiance !
	      var result = addition(nombre1, nombre2);
	      resolve(result);
	    } catch(error) {
	      reject(error);
	    }
	});
}

promiseAddition(4, 8)
	.then((result) => {
		console.log("Le résultat de 4 + 8 est", result);
	})
	.catch((error) => {
		console.log("Ooops", error);
	})

promiseAddition(5, "9")
	.then((result) => {
		console.log("Le résultat de 5 + 9 est", result);
	})
	.catch((error) => {
		console.log("Ooops", error);
	})

Console :

Déclencher une callback finale

Que l'on ait eu une erreur ou pas, il est possible qu'on souhaite quand même déclencher une sortie à notre promise.

L'API des Promises nous donne accès à la méthode finally qui est exécutée après les then ou catch.

promiseAddition(4, 8)
	.then((result) => {
		console.log("Le résultat de 4 + 8 est", result);
	})
	.catch((error) => {
		console.log("Ooops", error);
	})
	.finally(() => {
		console.log("Le processus d'addition 4 + 8 s'est terminé !");
	})


promiseAddition(5, "9")
	.then((result) => {
		console.log("Le résultat de 5 + 9 est", result);
	})
	.catch((error) => {
		console.log("Ooops", error);
	})
	.finally(() => {
		console.log("Le processus d'addition 3 + '9' s'est terminé !");
	})

Console :

Cette méthode est utile pour pouvoir enchaîner quoiqu'il arrive la suite d'un processus de traitement.

Certains cas métiers peuvent continuer sans dépendre d'une erreur !

Gestion du workflow

On a vu l'utilisation générale des Promises, maintenant il faut rentrer plus profondément dans le sujet.

Là où les callbacks étaient notre premier outil pour gérer du code asynchrone, avec toutes les défiances du callback hell, on pouvait gérer le workflow du traitement de données.

Les Promises nous aident à faire ce travail d'une façon bien plus élégante et plus robuste.

Dans l'API des Promises, il y a de nombreuses fonctionnalités qu'on peut utiliser pour faciliter le traitement de données.

Nous allons utiliser une librairie qui s'appelle bluebird. Certains exemples de cette partie l'utiliseront.

Il y a plusieurs manières de l'installer, si vous voulez l'avoir depuis un dépôt (CDN) ou en installant avec NodeJS.

Dans ce cas, je vais assumer que vous utilisez NodeJS pour vos projets.

L'installation via npm :

npm install --save bluebird

Bluebird apporte un grand nombre de fonctionnalités supplémentaires aux Promises, certains exemples l'importeront.

Enchaîner les promises

Maintenant que vous avez compris la base, allons plus loin pour utiliser l'intérêt réel des Promises.

Utilisons un exemple simple (basique).

On voudrait incrémenter une chaîne de caractère de plusieurs mots à partir de plusieurs Promises.

Par exemple, notre résultat final devrait être : "J'aime manger de la poutine parce que c'est bon !"

var phrase = "";

var promiseIncrement1 = function (value) {
  return new Promise((resolve, reject) => {
  	 // on incrémente la variable avec un nouveau texte
     value += "J'aime manger ";
     // on déclenche la fonction resolve avec la valeur qu'on veut passer à la promise suivante
     resolve(value);
  });
}

var promiseIncrement2 = function (value) {
  return new Promise((resolve, reject) => {
     value += "de la poutine ";
     resolve(value);
  });
}

var promiseIncrement3 = function (value) {
  return new Promise((resolve, reject) => {
     value += "parce que ";
     resolve(value);
  });
}

var promiseIncrement4 = function (value) {
  return new Promise((resolve, reject) => {
     value += "c'est bon !";
     resolve(value);
  });
}

//	Très facilement, on peut enchaîner les promises à partir d'une valeur initiale.
promiseIncrement1(phrase)
  // Notez bien que l'on n'a pas nécessairement besoin d'écrire une callback dans le then pour déclencher la promise
  // Le système de Promise se charge de passer le paramètre précédent issu d'un resolve à la fonction nécessitant un paramètre
  .then(promiseIncrement2)
  // On peut aussi écrire une callback si on en a besoin pour exécuter du code intermédiaire
  // Puis en retourne la promise en déclenchant la fonction qui la contient
  // Note : la callback peut être écrite comme une fonction anonyme ou arrow, pas d'inquiétude !
  .then((value) => {
  	console.log("Hello, je suis la promiseIncrement3 !");
    return promiseIncrement3(value);
  })
  .then(promiseIncrement4)
  .then((value) => {
      console.log("Ma phrase est : ", value);
  });

Console :

Contrairement à une approche avec des callback, il est facile d'éviter le fameux callback hell !

Démarrer un workflow à partir de données

Une autre manière d'écrire la partie précédente est d'utiliser Promise.resolve qui permet de créer automatiquement une promise réussie pour envoyer les données à la promise suivante.

Cette fonctionnalité permet d'avoir un code qui se rapproche davantage du data driven (basé sur les données).

import Promise from "bluebird";

//	C'est plus jolie, non ?
Promise.resolve(42)
	.then((result) => {
		console.log("La réponse à la vie, à l'univers et tout ce qui est : ", result);
	})

// Ce qui équivaut à :
const promise = new Promise((resolve, reject) => resolve(42));

promise.then((val) => {
	console.log("La réponse à la vie, à l'univers et tout ce qui est : ", result);
});

Déclencher plusieurs Promises

Imaginons le scénario suivant :

  • Lorsque votre application a un chargement, vous voudriez qu'un ensemble d'opérations se passent simultanément.
  • Vous déclenchez plusieurs promises, mais vous voulez que tout réponde seulement lorsque tous vos traitements sont terminés.

C'est là que Promise.all entre en jeu. La méthode Promise.all prend une série de promesses et déclenche une callback lorsqu'ils sont tous résolus :

import Promise from "bluebird";

var promise1 = new Promise(function(resolve, reject) {
	// On simule une attente
	setTimeout(() => { resolve('1er !'); }, 4000);  // Le résultat va arriver au bout de 4 secondes
});

var promise2 = new Promise(function(resolve, reject) {
	// On simule une attente
	setTimeout(() => { resolve('2ème !'); }, 3000);
});

Promise.all([promise1, promise2])
	.then((results) => {
		console.log('Résultat: ', results);
	})
	.catch((err) => {
		console.log('Erreur: ', err);
	});

Console :

Dans le cas où l'un des deux provoque une erreur, l'ensemble du Promise.all tombe dans le catch :

import Promise from "bluebird";

var promise1 = new Promise(function(resolve, reject) {
	// On simule une attente
	setTimeout(() => { resolve('1er !'); }, 4000);  // Le résultat va arriver au bout de 4 secondes
});

var promise2 = new Promise(function(resolve, reject) {
	// On simule une attente
	setTimeout(() => { reject('2ème !'); }, 3000);
});

Promise.all([promise1, promise2])
	.then((results) => {
		console.log('Résultat: ', results);
	}).catch((err) => {
		console.log('Erreur: ', err);
	});

Console :

Déclencher plusieurs Promises en parallèles

Promise.race est une méthode intéressante.

Au lieu d'attendre que toutes les promises soient résolues ou rejetées, Promise.race se déclenche dès qu'une promesse dans le tableau est résolue ou rejetée.

import Promise from "bluebird";

var promise1 = new Promise(function(resolve, reject) {
	setTimeout(() => { resolve('1er !'); }, 4000);
});
var promise2 = new Promise(function(resolve, reject) {
	setTimeout(() => { resolve('2ème !'); }, 2000);
});

Promise.race([ promise1, promise2 ])
	.then((one) => {
		console.log('Résultat : ', one);
	}).catch((error) => {
		console.log('Erreur : ', error);
	});

Console :

Dans la vraie vie, on peut faire une requête vers une source primaire et une source secondaire (dans le cas où le primaire ou le secondaire sont indisponibles).

Provoquer une attente

Dans un autre scénario, imaginons que vous souhaitiez attendre volontairement quelques secondes avant de déclencher une promise.

Promise.delay va vous permettre de spécifier un temps en millisecondes pour faire patienter la suite de votre workflow.

import Promise from "bluebird";

var promise1 = () => new Promise((resolve, reject) => {
	Promise.delay(2500)
		.then(() => {
			resolve("1er !");
		})
});

var promiseDelay = () => {
	return Promise.delay(2500);
}

var promise2 = () => new Promise((resolve, reject) => {
	Promise.delay(2500)
		.then(() => {
			resolve("2ème !");
		})
});

Promise.resolve()
	.then(() => {
		console.log("On démarre ?")
	})
	.then(promise1)
	.then((result) => {
		console.log("Promise 1 fini !", result)
	})
	.then(promiseDelay)
	.then(() => {
		console.log("Promise Delay fini !")
	})
	.then(promise2)
	.then((result) => {
		console.log("Résultat : ", result);
	})
	.catch((error) => {
		console.log('Erreur : ', error);
	});

Console :

Note : A savoir que then n'accepte pas les promises en objet, nous sommes obligés de fournir une fonction !

Utilisation des Promises avec des requêtes HTTP

Parce que vous allez être quelques-uns à vouloir savoir le faire, alors je vous spoil direct !

Je vous conseille de lire mon article sur Axios, vous saurez facilement comment l'installer, l'utiliser et combiner avec les promises.

Maiiiiis voici un petit exemple :

import axios from "axios";

var getPosts = () => {
	return new Promise((resolve, reject) => {
		axios({
			method: 'get',
			url: 'https://jsonplaceholder.typicode.com/posts'
		})
		.then((response) => resolve(response.data))
		.catch((error) => reject(new Error("Impossible de récupérer les posts")));
	})
}

getPosts()
	.then((posts) => {
		console.log("Nombres de posts :", posts.length);
		console.log("Premier post", JSON.stringify(posts[0]));
	})
	.catch((error) => {
		console.log("Erreur dans la requête", error);
	})

Console :

Conlusion

Les Promises nous donnent la possibilité d'écrire du code asynchrone de manière synchrone, avec indentation à plat et un seul canal d'exception.

Les Promises ont été un sujet brûlant ces dernières années.

Les développeurs sont capables d'éviter les callback hell et les interactions asynchrones peuvent être transmises comme n'importe quelle autre variable.

Les Promises nous aident à unifier les API asynchrones et nous permettre d'intégrer des API non conformes à base de callbacks.

Vous ne pouvez pas annuler une Promise, une fois créée, elle commencera à être exécutée.

Vous devez gérer les rejets ou les exceptions, obligatoirement.

Vous ne pouvez pas déterminer l'état d'une Promise, c'est-à-dire si elle est en attente, réussie ou rejetée. Ou même déterminer où il se trouve dans son traitement en état d'attente.

Amusez-vous bien avec ce concept ultra nécessaire à connaître.

function userInteraction(article) {

	const { user, david } = article.humans;

	if (user.likeArticle) {
		if (!user.isFanFacebookPage) {
			user.likeFacebookPage();
		}
		user.shareFacebook();
		user.shareTwitter();
		user.shareLinkedin();
		david.notifyThankYou();
	} else {
		david.noOffence();
		david.notifyStillLoveYou();
	}

}

userInteraction(this);
Partages ! 😉