Propagation des exceptions: quand devrais-je intercepter des exceptions?

44

MethodA appelle un MethodB qui appelle à son tour MethodC.

Il n'y a PAS de gestion des exceptions dans MethodB ou MethodC. Mais il existe une gestion des exceptions dans MethodA.

Dans MethodC, une exception se produit.

Maintenant, cette exception est en train de bouillonner jusqu'à MethodA, qui la gère correctement.

Quel est le probleme avec ca?

Dans mon esprit, à un moment donné, un appelant exécutera MethodB ou MethodC, et lorsque des exceptions surviennent dans ces méthodes, ce que l'on obtiendra de la gestion des exceptions à l'intérieur de ces méthodes, qui consiste essentiellement en un bloc try / catch / finally au lieu de simplement laisser les bulles à l'appelé?

La déclaration ou le consensus autour de la gestion des exceptions est à lancer lorsque l'exécution ne peut pas continuer pour cette raison - une exception. Je comprends ça. Mais pourquoi ne pas capturer l'exception plus haut dans la chaîne au lieu d'avoir des blocs try / catch tout en bas.

Je le comprends quand vous devez libérer des ressources. C'est une toute autre affaire.

Daniel Frost
la source
46
Pourquoi pensez-vous que le consensus est de mettre en place une chaîne de contrôle des prises?
Caleth
Avec un bon IDE et un bon style de codage, vous pouvez savoir qu’une exception peut être levée lorsqu’une méthode est appelée. Le manipuler ou le laisser se propager est la décision de l'appelant. Je ne vois pas de problème avec ça.
Hieu Le
14
Si une méthode ne peut pas gérer l'exception, et la rejette simplement, je dirais que c'est une odeur de code. Si une méthode ne peut pas gérer l'exception, et n'a rien d'autre à faire lorsqu'une exception est levée, aucun try-catchbloc n'est nécessaire .
Greg Burghardt
7
"Quel est le probleme avec ca ?" : rien
Ewan
5
L'interception de pass-through (qui n'englobe pas les exceptions dans différents types ou quelque chose du genre) va à l'encontre de l'objectif même des exceptions. Le lancement d'exceptions est un mécanisme complexe, et il a été construit intentionnellement. Si les captures directes étaient le cas d'utilisation prévu, il vous suffirait alors d'implémenter un Result<T>type (un type qui stocke le résultat d'un calcul ou une erreur) et de le renvoyer à partir de vos fonctions de lancement. La propagation d'une erreur dans la pile impliquerait de lire chaque valeur de retour, de vérifier si c'est une erreur et de renvoyer une erreur si c'est le cas.
Alexander

Réponses:

139

En règle générale, ne capturez pas les exceptions à moins de savoir quoi faire avec elles. Si MethodC lève une exception, mais que MethodB n'a aucun moyen utile de la gérer, elle devrait permettre à l'exception de se propager jusqu'à MethodA.

Les seules raisons pour lesquelles une méthode doit avoir un mécanisme de capture et de remise à zéro sont les suivantes:

  • Vous souhaitez convertir une exception en une exception plus significative pour l'appelant ci-dessus.
  • Vous souhaitez ajouter des informations supplémentaires à l'exception.
  • Vous avez besoin d'une clause de blocage pour nettoyer les ressources qui seraient divulguées sans une.

Sinon, intercepter des exceptions au mauvais niveau a tendance à produire un code qui échoue de manière silencieuse sans fournir de retour d'information utile au code appelant (et finalement à l'utilisateur du logiciel). L'alternative consistant à intercepter une exception puis à la rediffuser immédiatement est inutile.

Simon B
la source
28
@ GregBurghardt si votre langue a quelque chose de similaire try ... finally ..., utilisez-la plutôt que d'attraper et de relancer
Caleth
19
"attraper une exception puis la renvoyer immédiatement est inutile" En fonction de la langue et de la façon dont vous vous y prenez, cela peut nuire activement à la base de code. Les personnes qui tentent cette opération suppriment souvent de nombreuses informations sur l'exception, telle que le stacktrace d'origine. J'ai traité le code dans lequel l'appelant obtient une exception complètement trompeuse quant à ce qui s'est passé et où.
JimmyJames
7
"n'attrapez pas les exceptions à moins de savoir quoi faire avec elles". Cela semble raisonnable à première vue, mais pose des problèmes plus tard. Ce que vous faites ici divulgue des détails de mise en œuvre à vos appelants. Imaginez que vous utilisez un ORM particulier dans votre implémentation pour charger des données. Si vous ne détectez pas les exceptions spécifiques de cet ORM, mais les laissez simplement bouillonner, vous ne pouvez pas remplacer votre couche de données sans rompre la compatibilité avec les utilisateurs existants. C'est l'un des cas les plus évidents, mais cela peut devenir assez insidieux et difficile à appréhender.
Voo
11
@Voo Dans votre exemple , vous ne savez quoi faire. Enveloppez-le dans une exception documentée spécifique à votre code, par exemple, LoadDataExceptionet incluez les détails de l'exception d'origine en fonction de vos fonctionnalités linguistiques, afin que les futurs responsables puissent en identifier la cause sans avoir à attacher un débogueur et à comprendre comment reproduire le problème.
Colin Young
14
@Voo Vous semblez avoir manqué le motif "Vous voulez convertir une exception en une exception plus significative pour l'appelant ci-dessus" pour les scénarios de capture / réattribution.
jpmc26
21

Quel est le probleme avec ca ?

Absolument rien.

Maintenant, cette exception est en train de bouillonner jusqu'à MethodA, qui la gère correctement.

"le gère de manière appropriée" est la partie importante. C'est le noeud de la gestion des exceptions structurées.

Si votre code peut faire quelque chose "utile" avec une exception, allez-y. Si non, alors laissez bien seul.

. . . pourquoi ne pas capturer l'exception plus loin dans la chaîne au lieu d'avoir des blocs try / catch tout en bas.

C'est exactement ce que vous devriez faire. Si vous lisez du code dont le gestionnaire / renifleur est "tout en bas", alors vous êtes (probablement) en train de lire un code assez médiocre.

Malheureusement, certains développeurs voient dans les blocs catch comme un code "code chaud" qu'ils jettent (sans jeu de mots) dans toutes les méthodes qu'ils écrivent, souvent parce qu'ils ne "reçoivent" pas vraiment "Exception Handling" et pensent devoir ajouter quelque chose d'aussi que les exceptions ne "s'échappent" pas et ne tuent pas leur programme.

Une partie de la difficulté ici est que, la plupart du temps, ce problème ne sera même pas remarqué, car les exceptions ne sont pas lancées tout le temps, mais quand elles le seront , le programme va perdre énormément de temps et effort en décapitant graduellement la pile d’appel pour arriver à un endroit qui fait réellement quelque chose d’utile avec Exception.

Phill W.
la source
7
Ce qui est encore pire, c’est que l’application attrape l’exception, puis la consigne (dans l’espoir qu’elle ne restera pas là pour toujours) et tente de continuer comme d’habitude, même si elle ne le peut vraiment pas.
Salomon Ucko
1
@SolomonUcko: Eh bien, ça dépend. Si, par exemple, vous écrivez un serveur RPC simple et qu'une exception non gérée bouillonne jusqu'à la boucle d'événements principale, votre seule option raisonnable est de le consigner, d'envoyer une erreur RPC à l'homologue distant et de reprendre le traitement des événements. L’autre solution consiste à supprimer l’ensemble du programme, ce qui conduira les écrous de votre SRE à la mort du serveur en production.
Kevin
@Kevin Dans ce cas, il devrait y avoir un seul catchau plus haut niveau possible qui enregistre l'erreur et renvoie une réponse d'erreur. Pas des catchblocs éparpillés partout. Si vous ne souhaitez pas répertorier toutes les exceptions vérifiées possibles (dans des langages tels que Java), placez-les simplement dans un RuntimeExceptionfichier plutôt que de les consigner ici, en essayant de continuer et en générant davantage d'erreurs, voire de vulnérabilités.
Salomon Ucko
8

Vous devez faire la différence entre les bibliothèques et les applications.

Les bibliothèques peuvent lancer des exceptions non capturées librement

Lorsque vous concevez une bibliothèque, vous devez à un moment donné penser à ce qui peut mal tourner. Les paramètres peuvent être dans la mauvaise plage ou nulldes ressources externes peuvent ne pas être disponibles, etc.

Le plus souvent, votre bibliothèque n'aura pas le moyen de les traiter de manière sensée . La seule solution sensée est de lancer une exception appropriée et de laisser le développeur de l'application s'en occuper.

Les applications doivent toujours, à un moment donné, intercepter des exceptions

Quand une exception est interceptée, j'aime bien les classer comme des erreurs ou des erreurs fatales . Une erreur normale signifie qu'une seule opération dans mon application a échoué. Par exemple, un document ouvert n'a pas pu être enregistré car la destination n'était pas accessible en écriture. La seule pensée sensée pour l'application consiste à informer l'utilisateur que l'opération n'a pas pu être complétée avec succès, à donner des informations lisibles par l'homme concernant le problème, puis à laisser l'utilisateur décider de la suite.

Une erreur fatale est une erreur dont la logique principale de l'application ne peut pas récupérer. Par exemple, si le pilote du périphérique graphique se bloque dans un jeu vidéo, il n’ya aucun moyen pour l’application d’en informer «gracieusement» l’utilisateur. Dans ce cas, un fichier journal doit être écrit et, si possible, l'utilisateur doit être informé d'une manière ou d'une autre.

Même dans un cas aussi grave, l'application doit gérer cette exception de manière significative. Ceci peut inclure l'écriture d'un fichier journal, l'envoi d'un rapport d'incident, etc. Il n'y a aucune raison pour que l'application ne réponde en aucune façon à l'exception.

MechMK1
la source
En effet, si vous avez une bibliothèque pour quelque chose comme les opérations d'écriture sur disque ou, par exemple, toute autre manipulation matérielle, vous pouvez vous retrouver dans toutes sortes d'événements imprévus. Que se passe-t-il si un disque dur est retiré pendant l’écriture? Qu'est-ce qu'un lecteur de CD court-circuite pendant la lecture? Cela échappe à votre contrôle et même si vous pouvez faire quelque chose (par exemple, prétendre que cela a été un succès), il est souvent recommandé de laisser une exception à l'utilisateur de la bibliothèque et de le laisser décider. Peut-être qu'un HDDPluggedOutDuringWritingExceptionpeut être traité et n'est pas fatal pour l'application. Le programme peut décider quoi faire avec cela.
VLAZ
1
@VLAZ L'application doit décider ce qui est fatal ou non fatal. La bibliothèque devrait raconter ce qui s'est passé. L'application doit décider comment réagir.
MechMK1
0

Ce qui ne va pas avec le modèle que vous décrivez, c'est que la méthode A n'aura aucun moyen de distinguer trois scénarios:

  1. La méthode B a échoué de manière anticipée.

  2. La méthode C a échoué d’une manière qui n’était pas prévue par la méthode B, mais qui effectuait une opération pouvant être abandonnée en toute sécurité.

  3. La méthode C a échoué d’une manière qui n’était pas prévue par la méthode B, mais qui effectuait une opération qui a placé les objets dans un état supposé incohérent supposé être temporaire que B n’a pas nettoyé à cause de l’échec de C.

La seule façon dont la méthode A sera capable de distinguer ces scénarios sera si l'exception levée par B inclut des informations suffisantes à cette fin ou si la pile déroulée pour la méthode B fait que l'objet est laissé dans un état explicitement invalidé . Malheureusement, la plupart des structures d'exception rendent les deux modèles difficiles, ce qui oblige les programmeurs à prendre des décisions de conception "moins graves".

supercat
la source
2
Les scénarios 2 et 3 sont des bogues de la méthode B. La méthode A ne devrait pas tenter de les résoudre.
Sjoerd
@Sjoerd: Comment la méthode B est-elle supposée anticiper toutes les façons dont la méthode C peut échouer?
Supercat
Par des schémas bien connus, comme effectuer toutes les opérations susceptibles d’aboutir à des variables temporaires, puis avec des opérations qui ne peuvent pas exécuter - par exemple, échanger - échanger l’ancien avec le nouvel état. Un autre modèle consiste à définir des opérations pouvant être répétées en toute sécurité. Vous pouvez ainsi recommencer l'opération sans craindre de vous déranger. Il y a des livres complets sur l'écriture de 'code d'exception', je ne peux donc pas tout vous dire ici.
Sjoerd
C'est un bon point pour ne pas utiliser les exceptions du tout (ce qui serait une excellente décision à mon humble avis). Mais je suppose que cela ne répond pas vraiment à la question, car le PO semble vouloir utiliser des exceptions en premier lieu, et demande seulement la capture devrait être.
cmaster
@Sjoerd La méthode B devient beaucoup plus facile à raisonner si les exceptions sont interdites par le langage. Parce que dans ce cas, vous voyez en réalité tous les chemins de flux de contrôle via B, et vous n'avez pas à deviner quels opérateurs pourraient être surchargés (C ++) pour éviter le scénario 3. Nous payons beaucoup en termes de code la clarté et la sécurité afin d’être paresseux sur le renvoi des erreurs en "lançant" des exceptions. En fin de compte, la gestion des erreurs est une partie vitale du code.
cmaster