Déboguer les promesses en JavaScript

Le développeur adepte du débogueur de Firefox qui s'initie à la programmation asynchrone à l'aide de promesses de JavaScript peut assez rapidement en venir à s'arracher les cheveux.
En effet, le débogueur lui semblera parfois ne pas afficher des exceptions de syntaxe. Dur de localiser une erreur dans l'écriture du code et de la corriger dans ces conditions...
Déboguer les promesses en JavaScript
Cela tient à la manière assez particulière dont ce type d'exception est géré par le débogueur. Fort heureusement, il existe une solution technique... pour autant que l'on veille bien à la mettre en oeuvre !

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))

L'exception pour faute de syntaxe aux oubliettes

Le débogage des promesses est notoirement délicat, du fait que si un rejecter est utilisé, le navigateur n'affiche pas dans la console une exception aussi basique que celle signalant une faute de syntaxe dans l'executor.
Ici, Firefox affiche l'exception ("fuck is not defined") dans la console, car il n'y a pas de rejecter :
Promise.resolve ().then (result =>
	fuck
);
Pareil ici, car aucun rejecter n'est passé à .catch () :
Promise.resolve ().then (result =>
	fuck
).catch ();
Ici, Firefox n'affiche pas l'exception, considérant que le rejecter passé à .catch () l'a attrapée et va la gérer :
Promise.resolve ().then (result =>
	fuck
).catch (error =>
	console.log (error)	// Sans affichage explicite comme ici, l'exception n'est pas affichée dans la console
);
Noter que la signature du rejecter n'importe pas. Ici, Firefox n'affiche pas l'exception, alors même que le rejecter ne serait pas censé l'attraper, puisqu'il ne la prend pas en paramètre :
Promise.resolve ().then (result =>
	fuck
).catch (() => {});
Bref, tout se passe comme si le code de l'application résidait dans un bloc try...catch, si bien que si ce code est lui-même logé dans un bloc try...catch et que le code logé dans le catch ne la relance pas, l'exception ne parvient jamais au code de Firefox qui l'affiche :
try {
	try {
		// Code de l'executor
		fuck
	}
	catch (e) {	// Rejecter toujours susceptible d'attraper l'exception signalant une faute de syntaxe
		// Code du rejecter
		rethrow e	// Indispensable pour que Firefox attrape l'exception
	}
}
catch {
	// Code de Firefox pour afficher l'exception dans la console
}
La subtile différence avec la réalité, c'est qu'il impossible d'écrire le catch d'un bloc try...catch sous la forme catch (), c'est-à-dire sans paramètre. C'est soit catch pour attraper toute exception, soit catch (e) pour attraper toute exception et charge à l'utilisateur de les discriminer à l'aide de instanceof. Il faut donc lire... :
new Promise ((resolve, reject) => {}).catch (() => {})		// Pas d'exception dans la signature du rejecter
...comme :
new Promise ((resolve, reject) => {}).catch (e => {})		// Exception dans la signature du rejecter

Attraper et relancer l'exception signalant une faute de syntaxe

Le fait que Firefox puisse ne pas afficher une exception signalant une faute de syntaxe complique le débogage. Tout l'enjeu est de ne jamais oublier d'afficher l'erreur dans un rejecter. Un scénario ennuyeux est de l'oublier dans un enchaînement de promesses. Un exemple où l'exception part aux oubliettes à la fin de l'enchaînement :
function f () {
	console.log ("f ()");
	return (Promise.resolve ());
}
function g () {
	console.log ("g ()");
	var p = new Promise ((resolve, reject) => fuck);
	return (p);
}
f ().then (result => g ()).catch (error => {});		// Exception aux oubliettes
L'exception signalant une faute de syntaxe survient dans l'executor de la promesse renvoyée par g (), fonction appelée par le fulfiller de la promesse renvoyée par f (). Dans une telle configuration, l'exception remonte la chaîne des promesses jusqu'à être attrapée par un rejecter, et n'est finalement affichée par Firefox que si aucun ne l'attrape. Comme un rejecter est associé à la promesse renvoyée par f (), ce rejecter attrape l'exception. Dès lors, elle ne parvient pas à Firefox. L'exception reste dans les mains du rejecter, mais comme ce dernier n'en fait rien, elle part aux oubliettes.
Un autre exemple, où l'exception part aux oubliettes durant l'enchaînement :
function f () {
	return (Promise.resolve ());
}
function g () {
	return (new Promise ((resolve, reject) => fuck).catch (error => {}));	// Exception aux oubliettes
}
f ().then (result => g ()).catch (error => console.log (error));
L'exception signalant une faute de syntaxe est attrapée par le rejecter associé à la promesse renvoyée par g (), mais ce rejecter n'en fait rien : non seulement il ne l'affiche pas, mais il ne la relance même pas pour que le rejecter associé à la promesse renvoyée par f () l'attrape. De ce fait, l'exception part aux oubliettes. Il faut au moins que le rejecter dans g () relance l'erreur pour que l'autre rejecter l'attrape :
function g () {
	return (new Promise ((resolve, reject) => fuck).catch (error => { throw error }));
}
Déboguer les promesses en JavaScript