Comment écrire un bon message d'exception


101

Je suis en train de passer en revue le code et l'une des choses que je remarque est le nombre d'exceptions où le message d'exception semble juste répéter cette exception s'est produite. par exemple

throw new Exception("BulletListControl: CreateChildControls failed.");

Les trois éléments de ce message peuvent être résolus à partir du reste de l'exception. Je connais la classe et la méthode de la trace de pile et je sais que cela a échoué (car j'ai une exception).

Cela m'a fait réfléchir à quel message j'ai mis dans les messages d'exception. Je crée d'abord une classe d'exception, s'il n'en existe pas déjà, pour la raison générale (par exemple PropertyNotFoundException, le pourquoi ), puis, lorsque je la lance, le message indique ce qui ne va pas (par exemple, "Impossible de trouver la propriété 'IDontExist' sur le nœud 1234 "- le quoi ). Le où est dans le StackTrace. Le quand peut se retrouver dans le journal (le cas échéant). Le comment est pour le développeur de travailler (et de réparer)

Avez-vous d'autres conseils pour lancer des exceptions? Spécifiquement en ce qui concerne la création de nouveaux types et le message d'exception.


4
S'agit-il de fichiers journaux ou à présenter à l'utilisateur?
Jon Hopkins

5
Pour le débogage seulement. Ils peuvent se retrouver dans un journal. Ils ne seraient pas présentés à l'utilisateur. Je ne suis pas fan de la présentation de messages d'exception à l'utilisateur.
Colin Mackay

Réponses:


72

Je réponds davantage à ce qui vient après une exception: à quoi cela sert-il et comment le logiciel doit-il se comporter, que doivent faire vos utilisateurs avec l'exception? Au début de ma carrière, une technique géniale a été de signaler les problèmes et les erreurs en 3 parties: contexte, problème et solution. L'utilisation de cette procédure change énormément le traitement des erreurs et améliore considérablement le logiciel pour les opérateurs.

Voici quelques exemples.

Context: Saving connection pooling configuration changes to disk.
Problem: Write permission denied on file '/xxx/yyy'.
Solution: Grant write permission to the file.

Dans ce cas, l'opérateur sait exactement quoi faire et dans quel fichier doit être affecté. Ils savent également que les modifications apportées au regroupement des connexions n'ont pas pris et doivent être répétées.

Context: Sending email to 'abc@xyz.com' regarding 'Blah'.
Problem: SMTP connection refused by server 'mail.xyz.com'.
Solution: Contact the mail server administrator to report a service problem.  The email will be sent later. You may want to tell 'abc@xyz.com' about this problem.

J'écris des systèmes côté serveur et mes opérateurs bénéficient généralement d'un support technique de premier niveau. J'écrirais les messages différemment pour les logiciels de bureau qui s'adressent à un public différent mais qui incluent la même information.

Plusieurs choses merveilleuses se produisent si l'on utilise cette technique. Le développeur de logiciel est souvent le mieux placé pour savoir comment résoudre les problèmes dans son propre code. Encoder ainsi des solutions de cette manière est extrêmement avantageux pour les utilisateurs finaux désavantagés, car ils manquent souvent des informations sur qu'est-ce que le logiciel faisait exactement. Quiconque a déjà lu un message d'erreur Oracle saura ce que je veux dire.

La deuxième chose merveilleuse qui me vient à l’esprit est que vous essayez de décrire une solution dans votre exception et que vous écrivez «Check X and if A, B else C». Ceci est un signe très clair et évident que votre exception est vérifiée au mauvais endroit. En tant que programmeur, vous avez la capacité de comparer des éléments dans un code. Par conséquent, "si" les instructions doivent être exécutées dans un code, pourquoi impliquer l'utilisateur dans quelque chose qui peut être automatisé? Il y a des chances qu'il est de plus profond dans le code et quelqu'un a fait la chose paresseux et jeté IOException de tout nombre de méthodes et pris les erreurs potentielles de tous dans un bloc de code d' appel qui ne peut pas décrire adéquatement ce qui a mal tourné, ce que le particulierle contexte est et comment y remédier. Cela vous encourage à rédiger des erreurs de grain plus fines, à les capturer et à les manipuler au bon endroit dans votre code afin de pouvoir articuler correctement les étapes que l'opérateur doit suivre.

Dans une entreprise, nous avions des opérateurs de premier plan qui connaissaient très bien le logiciel et conservaient leur propre "livre de travail" qui augmentait nos rapports d'erreurs et nos solutions suggérées. Pour reconnaître cela, le logiciel a commencé à inclure des liens wiki vers le livre d'exécution dans les exceptions afin qu'une explication de base soit disponible, ainsi que des liens vers des discussions et des observations plus avancées par les opérateurs au fil du temps.

Si vous avez eu la dicipline d'essayer cette technique, il devient beaucoup plus évident de nommer vos exceptions dans le code lors de la création de vos propres. NonRecoverableConfigurationReadFailedException devient un raccourci pour ce que vous êtes sur le point de décrire plus en détail à l'opérateur. J'aime être bavard et je pense que ce sera plus facile à interpréter pour le prochain développeur qui manipule mon code.


1
+1 C'est un bon système. Qu'est-ce qui est le plus important: être sûr que l'information circule ou utiliser des mots courts?
Michael K

3
+1 pour j'aime la solution de l'inclusion, du contexte, du problème, de la solution
WebDev

1
Cette technique est tellement utile. Je vais certainement l'utiliser.
Kid Diamond

Le contexte est des données inutiles. Il est déjà présent dans la trace de la pile. Avoir une solution est préférable mais pas toujours possible / utile. La plupart des problèmes est une sorte d'arrêt grâce application ou ne tiennent pas compte des opérations en attente et retour à l' application principale boucle d'exécution dans l' espoir que vous réussir la prochaine fois ... Nom de la classe d'exception doit être telle que la solution devienne évidente, pour FileNotFoundou ConnectExceptionvous savez quoi faire))
Gavenkoa

2
@ThomasFlinkow Dans votre exemple, les traces auront init (), execute () et cleanup () dans la trace de la pile. Avec un bon schéma de nommage dans la bibliothèque et une API propre / compréhensible, vous n'avez pas besoin d'explications de chaîne. Et échouez vite, ne transportez pas d’états endommagés dans le système. Les traces et les enregistrements de journalisation avec ID unique peuvent expliquer le flux / l'état de l'application.
Gavenkoa

23

Dans cette question plus récente, j'ai souligné que les exceptions ne devraient contenir aucun message. À mon avis, le fait qu’ils le fassent est une idée fausse très répandue. Ce que je propose, c'est que

Le "message" de l'exception est le nom de classe (complet) de l'exception.

Une exception doit contenir dans ses propres variables membres autant de détails que possible sur ce qui s'est passé précisément. Par exemple, un IndexOutOfRangeExceptiondoit contenir la valeur d'index jugée non valide, ainsi que les valeurs supérieure et inférieure valides au moment où l'exception a été levée. De cette façon, en utilisant la réflexion, vous pouvez avoir un message automatiquement construit qui se lit comme ceci: IndexOutOfRangeException: index = -1; min=0; max=5et ceci, avec la trace de la pile, devrait être toutes les informations objectives dont vous avez besoin pour résoudre le problème. Le mettre en forme dans un joli message du type "index -1 n'était pas compris entre 0 et 5" n'ajoute aucune valeur.

Dans votre exemple particulier, la NodePropertyNotFoundExceptionclasse contiendrait le nom de la propriété non trouvée et une référence au nœud qui ne contiendrait pas la propriété. Ceci est important: il ne doit pas contenir le nom du noeud; il devrait contenir une référence au nœud réel. Dans votre cas particulier, cela peut ne pas être nécessaire, mais c'est une question de principe et une façon de penser préférée: la principale préoccupation lors de la construction d'une exception est qu'elle doit être utilisable par un code susceptible de l'intercepter. L'utilisabilité par l'homme est une préoccupation importante, mais seulement secondaire.

Cela résout la situation très frustrante à laquelle vous avez peut-être été témoin à un moment donné de votre carrière, où vous avez peut-être attrapé une exception contenant des informations vitales sur ce qui s'est passé dans le texte du message, mais pas dans ses variables de membre. Nous avons dû analyser le texte par des chaînes afin de comprendre ce qui s'est passé, en espérant que le texte du message reste le même dans les futures versions du calque sous-jacent, et en priant que le texte du message ne soit pas dans une langue étrangère lorsque votre programme est courir dans d'autres pays.

Bien sûr, étant donné que le nom de classe de l’exception est le message de l’exception (et que les variables membres de l’exception correspondent aux détails spécifiques), cela signifie que vous avez besoin de nombreuses exceptions pour transmettre tous les différents messages, et c'est bon.

Maintenant, parfois, alors que nous écrivons du code, nous rencontrons une situation erronée pour laquelle nous voulons simplement coder rapidement une throwinstruction et continuer à écrire notre code au lieu de devoir interrompre ce que nous faisons pour créer une nouvelle classe d'exceptions afin de pouvoir jetez-le là. Pour ces cas, j'ai une GenericExceptionclasse qui accepte effectivement un message de chaîne en tant que paramètre de construction, mais le constructeur de cette classe d'exception est orné d'un grand grand FIXME XXX TODOcommentaire violet clair indiquant que chaque instanciation de cette classe doit être remplacé par une instanciation d'une classe d'exception plus spécialisée avant la publication du système logiciel, de préférence avant la validation du code.


8
Si vous utilisez un langage dépourvu de GC, tel que C ++, vous devez faire très attention de ne pas insérer de références à des données arbitraires dans les exceptions que vous envoyez la pile. Les chances sont, tout ce que vous référencez a été détruit au moment où l'exception est capturée.
Sebastian Redl

4
@SebastianRedl True. Et la même chose pourrait s’appliquer à C # et à Java si l’ nodeobjet est protégé par une clause using-Disposable (en C #) ou try-with-resources (en Java): l’objet stocké avec l’exception serait supprimé / fermé, le rendant illégal. pour y accéder afin d’obtenir des informations utiles à l’endroit où l’exception est traitée. Je suppose que dans ces cas, une sorte de résumé de l'objet devrait être stocké dans l'exception, au lieu de l'objet lui-même. Je ne peux pas penser à un moyen infaillible de gérer cela de manière générique dans tous les cas.
Mike Nakis

13

En règle générale, une exception devrait aider les développeurs à identifier la cause en fournissant des informations utiles (valeurs attendues, valeur réelle, causes possibles / solutions, etc.).

De nouveaux types d'exception doivent être créés lorsqu'aucun des types intégrés n'a de sens . Un type spécifique permet aux autres développeurs d'attraper une exception spécifique et de la gérer. Si le développeur sait comment gérer votre exception mais que le type est Exception, il ne pourra pas le gérer correctement.


+1 - les valeurs attendues par rapport aux valeurs réelles sont extrêmement utiles. Dans l'exemple donné dans la question, vous ne devriez pas simplement dire qu'une méthode a échoué, mais pourquoi elle a échoué (en gros, la commande exacte qui a échoué et les circonstances qui ont provoqué l'échec.)
Felix Dombek Le

4

Dans .NET ne jamais throw new Exception("...")(comme l’a montré l’auteur de la question). L'exception est le type d'exception racine et n'est pas censée être lancée directement. Lancez plutôt l'un des types d'exception .NET dérivés ou créez votre propre exception personnalisée dérivée de Exception (ou d'un autre type d'exception).

Pourquoi ne pas lancer une exception? Parce que lancer Exception ne décrit en rien votre exception et oblige votre code d'appel à écrire du code comme si catch(Exception ex) { ... }ce n'était généralement pas une bonne chose! :-)


2

Les éléments que vous souhaitez "ajouter" à l'exception sont les éléments de données qui ne sont pas inhérents à l'exception ou à la trace de la pile. C’est une question intéressante de savoir si ces éléments font partie du "message" ou doivent être joints quand ils sont connectés.

Comme vous l'avez déjà noté, l'exception vous indique probablement quoi, le stacktrace vous indique probablement où mais le "pourquoi" peut être plus impliqué (devrait l'être, on peut l'espérer) que d'aller simplement regarder une ligne ou deux et dire " doh! Bien sur ". C’est encore plus vrai lorsque nous enregistrons des erreurs dans le code de production - j’ai trop souvent été piqué par de mauvaises données qui se sont retrouvées dans un système réel qui n’existe pas dans nos systèmes de test. Une simple simple idée de savoir quel est l'identifiant de l'enregistrement dans la base de données qui cause (ou contribue) à l'erreur peut faire gagner un temps considérable.

Alors ... Soit la liste ou, pour .NET, ajoutée à la collecte de données sur les exceptions consignées (cf @Plip!):

  • Paramètres (cela peut devenir un peu intéressant - vous ne pouvez pas ajouter de données à la collecte de données si elle ne se sérialise pas et parfois, un seul paramètre peut être étonnamment complexe)
  • Les données supplémentaires renvoyées par ADO.NET ou Linq vers SQL ou similaire (cela peut aussi être un peu intéressant!).
  • Tout ce qui pourrait ne pas être apparent.

Certaines choses, bien sûr, vous ne saurez pas ce dont vous avez besoin tant que vous ne les aurez pas dans votre rapport / journal d’erreur initial. Certaines choses que vous ne réalisez pas que vous pouvez obtenir jusqu'à ce que vous trouviez que vous en avez besoin.


0

Quelles sont les exceptions pour ?

(1) Dire à l'utilisateur que quelque chose s'est mal passé?

Cela devrait être un dernier recours, parce que votre code devrait intercéder et leur montrer quelque chose de "plus beau" qu'une exception.

Le message "erreur" doit indiquer clairement et succinctement ce qui ne va pas et ce que l'utilisateur peut éventuellement faire pour remédier à la situation.

par exemple "S'il vous plaît, n'appuyez plus sur ce bouton"

(2) Dire à un développeur quand il a mal tourné?

C’est le genre de chose que vous enregistrez dans un fichier pour une analyse ultérieure.
La trace de pile indiquera au développeur le code a été cassé; le message devrait, encore une fois, indiquer ce qui a mal tourné.

(3) Dire à un gestionnaire d'exception (un code) que quelque chose s'est mal passé?

Le type de l'exception déterminera quel gestionnaire d'exception pourra l'examiner et les propriétés définies sur l'objet exception permettront au gestionnaire de s'en occuper.

Le message de l'exception est totalement hors de propos .


-3

Ne créez pas de nouveaux types si vous pouvez y contribuer. Ils peuvent provoquer une confusion et une complexité supplémentaires et entraîner davantage de code à maintenir. Ils peuvent obliger votre code à s'étendre. La conception d'une hiérarchie d'exceptions nécessite beaucoup de tests et de tests. Ce n'est pas une pensée après. Il est souvent préférable d'utiliser la hiérarchie des exceptions de langage intégrée.

Le contenu du message d'exception dépend du destinataire du message - vous devez donc vous mettre à la place de cette personne.

Un ingénieur du support technique devra être capable d'identifier la source de l'erreur le plus rapidement possible. Incluez une courte chaîne descriptive ainsi que toutes les données pouvant aider à résoudre le problème. Toujours inclure la trace de la pile - si vous le pouvez - ce sera la seule véritable source d’information.

La présentation des erreurs aux utilisateurs généraux de votre système dépend du type d'erreur: si l'utilisateur peut résoudre le problème, en fournissant une entrée différente, un message descriptif concis est requis. Si l'utilisateur ne peut pas résoudre le problème, il est préférable de signaler qu'une erreur s'est produite et de connecter / envoyer une erreur au support (en suivant les instructions ci-dessus).

De plus, ne lancez pas une grosse "HALT ERROR!" icône. C'est une erreur - ce n'est pas la fin du monde.

En résumé, réfléchissez aux acteurs et aux cas d'utilisation de votre système. Mettez-vous à la place de l'utilisateur. Soit utile. Sois gentil. Pensez-y dès le départ dans la conception de votre système. Du point de vue de vos utilisateurs - ces cas exceptionnels et la façon dont le système les gère sont tout aussi importants que les cas normaux de votre système.


10
Je ne suis pas d'accord. Il existe de nombreuses bonnes raisons d'implémenter vos propres exceptions lorsque l'API de langage ne répond pas à vos besoins. Une des raisons est que dans une méthode où plusieurs choses peuvent échouer, vous pouvez écrire différentes clauses catch pour différents types d'exceptions, où vous pouvez réagir au problème exact. Une autre solution est que vous pouvez séparer plusieurs couches d'exceptions représentant différentes couches d'abstraction, où la couche exacte à laquelle appartient une exception peut être codée dans son type. Le simple fait d'utiliser "Exception" ou "IllegalStateException" avec une chaîne de message n'aide pas beaucoup.
Felix Dombek

1
Je ne suis pas d'accord non plus. Les types d'exception doivent avoir un sens pour le code appelant qui le consommera. Par exemple, si j'appelle un framework et que cela provoque une exception FileDoesNotExistException en interne, cela n'a aucun sens pour moi en tant qu'appelant du framework. Au lieu de cela, il peut être préférable de créer une exception personnalisée et de transmettre l’exception levée en tant qu’exception interne.
bytedev
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.