Il existe de nombreux tutoriels sur la manière d'utiliser "then" et "catch" lors de la programmation avec JavaScript Promise. Cependant, tous ces tutoriels semblent manquer un point important: revenir d'un bloc then/catch pour rompre la chaîne Promise. Commençons par un code synchrone pour illustrer ce problème:
try {
someFunction();
} catch (err) {
if (!(err instanceof MyCustomError))
return -1;
}
someOtherFunction();
En substance, je teste une erreur interceptée et si ce n'est pas l'erreur, je pense que je reviendrai à l'appelant, sinon le programme se poursuivra. Cependant, cette logique ne fonctionnera pas avec Promise:
Promise.resolve(someFunction).then(function() {
console.log('someFunction should throw error');
return -2;
}).catch(function(err) {
if (err instanceof MyCustomError) {
return -1;
}
}).then(someOtherFunction);
Cette logique est utilisée pour certains de mes tests unitaires où je souhaite qu'une fonction échoue d'une certaine manière. Même si je change la capture en un bloc then, je ne suis toujours pas en mesure de rompre une série de promesses chaînées, car tout ce qui est renvoyé du bloc then/catch deviendra une promesse qui se propage le long de la chaîne.
Je me demande si Promise est capable de réaliser cette logique. sinon pourquoi? C'est très étrange pour moi qu'une chaîne Promise ne puisse jamais être brisée. Merci!
Edition le 16/08/2015: En fonction des réponses données jusqu'à présent, une promesse rejetée retournée par le bloc then se propage dans la chaîne de promesse et ignore tous les blocs suivants jusqu'à ce qu'elle soit interceptée (traitée). Ce comportement est bien compris car il imite simplement le code synchrone suivant (approche 1):
try {
Function1();
Function2();
Function3();
Function4();
} catch (err) {
// Assuming this err is thrown in Function1; Function2, Function3 and Function4 will not be executed
console.log(err);
}
Cependant, je demandais le scénario suivant en code synchrone (approche 2):
try {
Function1();
} catch(err) {
console.log(err); // Function1's error
return -1; // return immediately
}
try {
Function2();
} catch(err) {
console.log(err);
}
try {
Function3();
} catch(err) {
console.log(err);
}
try {
Function4();
} catch(err) {
console.log(err);
}
Je voudrais traiter différemment les erreurs soulevées dans différentes fonctions. Il est possible que j'attrape toutes les erreurs dans un bloc catch, comme illustré dans l'approche 1. Mais de cette façon, je dois faire une grosse instruction switch dans le bloc catch pour différencier les différentes erreurs; de plus, si les erreurs générées par différentes fonctions n'ont pas d'attribut commutable commun, je ne serai pas en mesure d'utiliser l'instruction switch; dans une telle situation, je dois utiliser un bloc try/catch distinct pour chaque appel de fonction. L’approche 2 est parfois la seule option. Promise ne supporte-t-il pas cette approche avec sa déclaration then/catch?
Cela ne peut pas être réalisé avec des fonctionnalités de la langue. Cependant, des solutions basées sur des modèles sont disponibles.
Voici deux solutions.
Rethrow erreur précédente
Ce modèle est fondamentalement sain ...
Promise.resolve()
.then(Function1).catch(errorHandler1)
.then(Function2).catch(errorHandler2)
.then(Function3).catch(errorHandler3)
.then(Function4).catch(errorHandler4)
.catch(finalErrorHandler);
Promise.resolve()
n'est pas strictement nécessaire, mais permet à toutes les lignes .then().catch()
de suivre le même motif et l'expression est plus simple à regarder.
... mais :
Le saut souhaité de la chaîne ne se produira que si les gestionnaires d'erreur sont écrits de manière à pouvoir faire la distinction entre une erreur précédemment générée et une erreur qui vient d'être générée. Par exemple :
function errorHandler1(error) {
if (error instanceof MyCustomError) { // <<<<<<< test for previously thrown error
throw error;
} else {
// do errorHandler1 stuff then
// return a result or
// throw new MyCustomError() or
// throw new Error(), new RangeError() etc. or some other type of custom error.
}
}
Maintenant :
if(error instanceof MyCustomError)
(par exemple, un .catch () final).Ce modèle serait utile si vous avez besoin de flexibilité pour passer en fin de chaîne ou non, en fonction du type d'erreur renvoyé. Circonstances rares je m'attends.
Captures isolées
Une autre solution consiste à introduire un mécanisme permettant de garder chaque .catch(errorHandlerN)
"isolée" de manière à ne capturer que les erreurs résultant de son correspondant FunctionN
, et non d'aucune erreurs précédentes.
Ceci peut être réalisé en ayant dans la chaîne principale uniquement des gestionnaires de succès, chacun comprenant une fonction anonyme contenant une sous-chaîne.
Promise.resolve()
.then(function() { return Function1().catch(errorHandler1); })
.then(function() { return Function2().catch(errorHandler2); })
.then(function() { return Function3().catch(errorHandler3); })
.then(function() { return Function4().catch(errorHandler4); })
.catch(finalErrorHandler);
Ici Promise.resolve()
joue un rôle important. Sans lui, Function1().catch(errorHandler1)
serait dans la chaîne principale, la catch()
ne serait pas isolée de la chaîne principale.
Maintenant,
Utilisez ce modèle si vous souhaitez toujours passer à la fin de la chaîne, quel que soit le type d'erreur généré. Un constructeur d'erreur personnalisé n'est pas requis et les gestionnaires d'erreur n'ont pas besoin d'être écrits de manière spéciale.
Cas d'utilisation
Quel modèle choisir sera déterminé par les considérations déjà données mais aussi éventuellement par la nature de votre équipe de projet.
Tout d'abord, je vois une erreur commune dans cette section de code qui pourrait être complètement déroutante. Ceci est votre exemple de bloc de code:
Promise.resolve(someFunction()).then(function() {
console.log('someFunction should throw error');
return -2;
}).catch(function(err) {
if (err instanceof MyCustomError) {
return -1;
}
}).then(someOtherFunction());
Vous devez transmettre les références de fonction à un gestionnaire .then()
, sans appeler réellement la fonction et renvoyer le résultat. Donc, ce code ci-dessus devrait probablement être ceci:
Promise.resolve(someFunction()).then(function() {
console.log('someFunction should throw error');
return -2;
}).catch(function(err) {
if (err instanceof MyCustomError) {
// returning a normal value here will take care of the rejection
// and continue subsequent processing
return -1;
}
}).then(someOtherFunction); // just pass function reference here
Notez que j'ai supprimé ()
Après les fonctions dans le gestionnaire .then()
, de sorte que vous ne faites que passer la référence de la fonction, sans l'appel immédiat. Cela permettra à l’infrastructure de la promesse de décider si elle l’appellera ou non à l’avenir. Si vous commettiez cette erreur, cela vous décontenancerait de la façon dont les promesses se dérouleraient, car les appels se feront quand même.
Trois règles simples pour attraper les rejets.
Vous pouvez voir quelques exemples dans this jsFiddle où il montre trois situations:
Le renvoi d'une valeur normale à partir d'un gestionnaire de rejet provoque l'appel du gestionnaire de résolution .then()
suivant (par exemple, le traitement normal se poursuit),
Le lancement d'un gestionnaire de rejet entraîne l'arrêt du traitement de résolution normal et tous les gestionnaires de résolution sont ignorés jusqu'à ce que vous obteniez un gestionnaire de rejet ou la fin de la chaîne. C'est un moyen efficace d'arrêter la chaîne si une erreur inattendue est trouvée dans un gestionnaire de résolution (ce que je pense est votre question).
En l'absence d'un gestionnaire de rejet présent, le traitement de résolution normal est arrêté et tous les gestionnaires de résolution sont ignorés jusqu'à ce que vous obteniez un gestionnaire de rejet ou la fin de la chaîne.
Il n'y a pas de fonctionnalité intégrée pour ignorer l'intégralité de la chaîne restante que vous demandez. Cependant, vous pouvez imiter ce comportement en générant une certaine erreur dans chaque capture:
doSomething()
.then(func1).catch(handleError)
.then(func2).catch(handleError)
.then(func3).catch(handleError);
function handleError(reason) {
if (reason instanceof criticalError) {
throw reason;
}
console.info(reason);
}
Si l'un des blocs catch attrape un criticalError
, il saute directement à la fin et renvoie l'erreur. Toute autre erreur serait consignée sur la console et avant de passer à la prochaine .then
bloquer.