Chaîner les promesses en JavaScript

Comme cela a été expliqué dans cet article, l'intérêt des promesses en JavaScript, c'est de permettre au développeur de pratiquer l'asycnhronisme sans tomber dans le "callback hell".
Pour autant, cela suppose de bien comprendre le fonctionnement de ce mécanisme alternatif, tout particulièrement cet aspect essentiel qu'est l'enchaînement, ou le chaînage, de promesses.
Pratique du chaînage de promesses en JavaScript
En cette matière, un examen trop superficiel du mécanisme peut conduire à s'en faire une idée fausse, et de là mal pratiquer le chaînage de promesses via .then () et .catch ().
Pour vous permettre d'honorer vos promesses, voici quelques éclaircissements pratiques...

Avertissements

Pour comprendre ce qui suit, il est impératif de savoir en quoi consiste l'asynchronisme en JavaScript, et à quoi sert une promesse dans ce contexte. Pour cela, le lecteur pourra se référer à cet article.
Pour simplifier le code donné en exemple, des promesses d'office résolues ou rejetées seront créées. Pour rappel :
  • Promise.resolve (7) permet de créer une promesse résolue comme new Promise ((resolve, reject) => resolve (7))
  • Promise.reject (666) permet de créer une promesse rejetée comme new Promise ((resolve, reject) => reject (666))

Le cas d'école : .then ().catch ()

Comment l'enchaînement f ().then (g).catch (h) est-il traité ? Si f () retourne une promesse rejected, seule h () est appelée, ce qui donne l'impression que .then () a été ignoré :
function f () {
	var value = 666;
	console.log (`f (): ${value}`);
	return (Promise.reject (value));
}
function g (value) {
	value ++;
	console.log (`g (): ${value}`);
	return (value);
}
function h (value) {
	value ++;
	console.log (`h (): ${value}`);
}
f ().then (g).catch (h);
f () : 666
h () : 667
En fait, .then () n'est pas ignoré : .then () n'est pas quelque chose que l'interpréteur traite comme autre chose qu'un appel de méthode. Simplement, s'agissant de l'appel à une méthode d'une promesse rejected, .then () se contente de renvoyer une promesse équivalente, plutôt que d'appeler le fulfiller qui lui était transmis et de retourner une promesse fabriquée à partir du résultat que ce dernier lui retourne, soit pour rappel :
  • une promesse équivalente si le résultat est une promesse (ie : un objet "thenable") ;
  • une promesse fulfilled avec le résultat si le résultat n'est pas une promesse.
.then () et .catch () créent des promesses
Il est fait mention à une promesse "équivalente". En effet, la promesse retournée par .then () est une nouvelle promesse, même si le fulfiller a retourné une promesse, et ce quelle que soit l'état de cette dernière.
Noter qu'il n'est pas évident de le vérifier empiriquement. En effet, la tentation serait d'écrire :
var p;
var p0 = Promise.resolve (42);
var p1 = p0.then (result => { p = Promise.resolve (result); return (p); });
console.log p, p1, p === p1);
Le problème, c'est que "run-to-completion" oblige, le fulfiller spécifié dans p0.then () n'aura pas été exécuté au moment du console.log (), si bien que p vaudra undefined. Il faut donc procéder autrement :
var p;
var p0 = Promise.resolve (42);
var p1 = p0.then (result => { p = Promise.resolve (result); return (p); });
p1.then (result => console.log (p, p1, p === p1));
Promise { : "fulfilled", : 42 }
Promise { : "fulfilled", : 42 }
false
Il en va de même avec .catch (), comme il est possible de le vérifier ainsi :
var p;
var p0 = Promise.reject (42);
var p1 = p0.catch (result => { p = Promise.resolve (result); return (p); });
p1.then (result => console.log (p, p1, p === p1));
Promise { : "fulfilled", : 42 }
Promise { : "fulfilled", : 42 }
false
Un .catch () se comporte de la même manière avec le résultat que le rejecter qui lui est transmis lui retourne. Ceci explique que dans le cas présent, un .catch () chaîné au .catch () ne produit rien :
f ().catch (g).catch (h);
f () : 666
g () : 667
En effet, le fulfiller g () retournant une valeur et non une promesse, le premier .catch () retourne une promesse fulfilled avec cette valeur. Le second .catch () étant appelé en tant que méthode de cette promesse, il n'appelle pas le rejecter qui lui est transmis, puisque cette promesse est fulfilled : il se contente de renvoyer une promesse équivalente.
Ce cas où .catch () n'est qu'un passe-plat peut être illustré en modifiant f () pour lui faire retourner une promesse fulfilled et chaînant d'abord un .catch () puis un .then () :
function f () {
	var value = 666;
	console.log (`f (): ${value}`);
	return (Promise.resolve (value));
}
function g (value) {
	value ++;
	console.log (`g (): ${value}`);
	return (value);
}
function h (value) {
	value ++;
	console.log (`h (): ${value}`);
}
f ().catch (g).then (h);
f () : 666
h () : 667
Pour revenir à l'exemple initial, si f () retourne une promesse rejected, ce n'est donc pas un .catch () mais .then () qu'il faut chaîner au .catch () pour poursuivre :
f ().catch (g).then (h);
f () : 666
g () : 667
h () : 668
Plus généralement, dans une chaîne, tous les .then () et les .catch () sont toujours bien appelés et retournent toujours chacun une promesse, mais ils n'appellent pas nécessairement le fulfiller / rejected qui leur est transmis : tout dépend de l'état de leur promesse.
Chaîner les promesses en JavaScript