web-dev-qa-db-fra.com

Comment revenir d'une capture / d'un blocage d'une promesse

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?

42
lixiang

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 :

  • si un gestionnaire d'erreur renvoie un résultat, la chaîne passe au gestionnaire de succès de la ligne suivante.
  • si errorHandler renvoie, la chaîne passe au gestionnaire d'erreur de la ligne suivante.

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 :

  • si un gestionnaire d'erreur renvoie un résultat, la chaîne passe à la prochaine FunctionN.
  • si un gestionnaire d'erreur lance une erreur MyCustomError, il sera répété de manière répétée dans la chaîne et intercepté par le premier gestionnaire d'erreur non conforme au protocole if(error instanceof MyCustomError) (par exemple, un .catch () final).
  • si un gestionnaire d'erreur génère un autre type d'erreur, la chaîne passe à la capture suivante.

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.

DÉMO

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,

  • si un gestionnaire d'erreur renvoie un résultat, la chaîne passe à la ligne suivante.
  • si un gestionnaire d'erreur lance quelque chose qui lui plaît, la chaîne passe directement au finalErrorHandler.

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.

DÉMO

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.

  • Equipe d'une personne - vous écrivez tout et comprenez les problèmes - si vous êtes libre de choisir, courez avec vos préférences personnelles.
  • Équipe multi-personnes - une personne écrit la chaîne principale et plusieurs autres personnes écrivent les fonctions et leurs gestionnaires d'erreur - si vous le pouvez, optez pour les prises isolées - avec tout sous le contrôle de la chaîne principale, vous n'avez pas à appliquer la discipline de écrire les gestionnaires d'erreur de cette certaine manière.
62
Roamer-1888

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.

  1. Si personne n'attrape le rejet, cela arrête immédiatement la chaîne de promesses et le rejet initial devient l'état final de la promesse. Aucun gestionnaire ultérieur n'est appelé.
  2. Si le rejet de la promesse est intercepté et que rien n'est renvoyé ou si aucune valeur normale n'est renvoyée par le gestionnaire de rejet, le rejet est considéré comme géré, la chaîne de promesse se poursuit et les gestionnaires suivants sont invoqués. Tout ce que vous retournez du gestionnaire de rejet devient la valeur actuelle de la promesse et comme si le rejet ne s'était jamais produit (à l'exception de ce niveau de gestionnaire de résolution qui n'a pas été appelé - le gestionnaire de rejet a été appelé à la place).
  3. Si le rejet de la promesse est intercepté et que vous émettez une erreur du gestionnaire de rejet ou que vous renvoyez une promesse refusée, tous les gestionnaires de résolution sont ignorés jusqu'au prochain gestionnaire de rejet de la chaîne. S'il n'y a pas de gestionnaires de rejet, la chaîne de promesse est arrêtée et l'erreur nouvellement créée devient l'état final de la promesse.

Vous pouvez voir quelques exemples dans this jsFiddle où il montre trois situations:

  1. 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),

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

  3. 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.

12
jfriend00

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.

4
rrowland