Accueil > Développement, Divers > Gestion des exceptions

Gestion des exceptions

Je bosse en ce moment sur une application assez âgée.
Elle est un peu dans son troisième âge (> 10 ans) avec un nombre important de personnes ont travaillés dessus (chacun ayant fait à sa sauce).
Comme toute application legacy, il y a beaucoup de choses qui sont à redire.

Dernièrement, on a eu une exception un peu obscure qui nous a donné un peu de fil à retordre.
Le premier réflexe a été de regardé dans le journal d’événements (puisqu’on logue dedans) pour savoir ce qu’il se passait.
Et là, c’est un peu le drame : rien d’exploitable. La StackTrace était tronquée, il n’y avait aucune réelle information…

Après avoir râlé et pesté tout ce que je pouvais (et accessoirement résoudre le problème), il m’a été demandé de faire un état des lieux de la gestion d’exceptions et faire des propositions pour améliorer les choses (la prochaine fois, je penserais à la fermer).

Ce billet est le fruit de ce travail (en plus court), avec tout plein de références.

 

Comment relancer une exception ?

 

Commençons par des rappels, puisque les choses ne sont pas forcément très clair.

Pour remonter une exception, il y a plusieurs choix.
Le premier est de créer une nouvelle exception :

catch (Exception ex)
{
    throw new Exception("Oups!", ex);
}

La deuxième est de relancer l’exception :

catch (Exception ex)
{
    throw ex;
}

Et la troisième de relancer mais en laissant le Framework se débrouiller :

catch (Exception ex)
{
    throw;
}

Dans les trois cas précédent, seul le dernier est valable.
Si on affiche la StackTrace à chaque fois, ça donne ça :

==========================================Throw EX
   à SampleExceptions.BatchThrowEx.Launch() dans c:\Users\fabien.guyot\Documents\Visual Studio 2012\Projects\SampleExceptions\SampleExceptions\BatchThrowEx.cs:ligne 18
   à SampleExceptions.Program.Main(String[] args) dans c:\Users\fabien.guyot\Documents\Visual Studio 2012\Projects\SampleExceptions\SampleExceptions\Program.cs:ligne 12

==========================================Throw New
   à SampleExceptions.BatchThrowNew.Launch() dans c:\Users\fabien.guyot\Documents\Visual Studio 2012\Projects\SampleExceptions\SampleExceptions\BatchThrowNew.cs:ligne 18
   à SampleExceptions.Program.Main(String[] args) dans c:\Users\fabien.guyot\Documents\Visual Studio 2012\Projects\SampleExceptions\SampleExceptions\Program.cs:ligne 22

==========================================Throw
   à System.Linq.Enumerable.Where[TSource](IEnumerable`1 source, Func`2 predicate)
   à SampleExceptions.ObjectManager.DoSomething(SampleObject obj) dans c:\Users\fabien.guyot\Documents\Visual Studio 2012\Projects\SampleExceptions\SampleExceptions\ObjectManager.cs:ligne 10
   à SampleExceptions.BatchThrow.Launch() dans c:\Users\fabien.guyot\Documents\Visual Studio 2012\Projects\SampleExceptions\SampleExceptions\BatchThrow.cs:ligne 18
   à SampleExceptions.Program.Main(String[] args) dans c:\Users\fabien.guyot\Documents\Visual Studio 2012\Projects\SampleExceptions\SampleExceptions\Program.cs:ligne 32

Comme on peut le voir, seule le « throw » conserve la StackTrace intacte et permet de remonter réellement à la source de l’exception.

Note : quand on logue une exception du Framework, il est pertinent d’utiliser la méthode ToString() qui va formater le texte avec toutes les InnerException (types d’exception, messages et StackTrace), plutôt que de vouloir réécrire le truc.

 

Pourquoi relancer une exception ?

 

Les cas précédents sont un peu stupides.
Sans aucun try/catch, pour la même exception, on a :

==========================================Sans rien
   à System.Linq.Enumerable.Where[TSource](IEnumerable`1 source, Func`2 predicate)
   à SampleExceptions.ObjectManager.DoSomething(SampleObject obj) dans c:\Users\fabien.guyot\Documents\Visual Studio 2012\Projects\SampleExceptions\SampleExceptions\ObjectManager.cs:ligne 10
   à SampleExceptions.BatchThrow.Launch() dans c:\Users\fabien.guyot\Documents\Visual Studio 2012\Projects\SampleExceptions\SampleExceptions\BatchThrow.cs:ligne 18
   à SampleExceptions.Program.Main(String[] args) dans c:\Users\fabien.guyot\Documents\Visual Studio 2012\Projects\SampleExceptions\SampleExceptions\Program.cs:ligne 42

A savoir la StackTrace complète.

Donc, il faut qu’un try/catch soit utile !
C’est à dire qu’il ne fasse pas QUE relancer l’exception.

D’ailleurs, Resharper va le signaler :
Catch Redundant

Ce qui veut dire que avec ou sans, ça fait pareil (alors autant ne pas le faire).

Ainsi, il convient d’abord de se poser des questions simples :
Que dois-je faire en cas d’exception ?
Remonter des informations à l’utilisateur ?
Loguer des informations ?
Où loguer les informations ?
Dois-je intercepter des exceptions dans toutes les couches de mon application ?
Dois-je loguer les exceptions dans toutes les couches de mon application ?

 

Remonter de l’informations à l’utilisateur

 

Côté utilisateur, il vaut mieux ne rien mettre de significatif.
Etre trop spécifique dans les messages d’erreur à destination de l’utilisateur final est une vulnérabilité : Information Leakage.

Le mieux est d’avoir un message générique avec des informations basiques qui vont permettre de retrouver l’exception.
Par informations basiques, j’entend par exemple le nom du compte, la page en erreur et l’heure de l’erreur.
Cela va permettre à l’utilisateur de faire une capture d’écran et de l’envoyer avec un message du genre « ça marche pas ».
Si les exceptions sont bien loguées, retrouver l’exception dans le journal d’événement (par exemple) ne sera pas trop trop compliquée.

 

Ajouter des informations

 

Déjà, il peut être pertinent d’ajouter du détail.
Pour cela, il y a une collection Exception.Data. C’est une collection clef/valeur (type object/object). Elle peut donc facilement être alimentée avec des données contextuelles.
Une moulinette simpliste pour alors extraire les clefs/valeurs pour les loguer.

Utiliser la collection Data plutôt que le Message est une préférence personnelle.
Déjà parce qu’avoir un message court et explicite permet de grouper plus facilement les exceptions.
J’ai déjà vu des messages avec tout une requête SQL généré par Entity Framework (ce n’était pas lui qui loguait), eh bah ça devient juste compliqué à lire…

Il ne faut pas oublier que le journal d’événements

Note :
Je parle, par exemple, d’une petite application qui va lire le journal et agréger les exceptions.
Ce qui permet éventuellement de voir quelles exceptions se produisent très souvent. Cela permet d’être éventuellement pro-actif (tout dépend la fréquence d’analyse) dans la résolution des anomalies mais aussi de les caractériser. Ainsi, une anomalie se produisant une fois par an sur un composant non critique pourra être considérée comme moins importante qu’une exception se produisant tous les jours (même si le composant n’est pas non plus critique, il en va du confort utilisateur).

 

Tester les valeurs

 

Il y a deux patterns pour tester des valeurs : le premier est le try/parse pattern et le second le tester/doer pattern.

Le premier peut être utilise ainsi :

var input = "007";

int code;
try
{
    code = int.Parse(input);
}
catch (FormatException)
{
    code = -1;
}
var input = "007";

int code;
if(!int.TryParse(input, out code))
{
    code = -1;
}
// ou
//int code = -1;
//int.TryParse(input, out code);

Basiquement, les deux vont faire appel à la classe interne Number, respectivement ParseInt32 et TryParseInt32.
Dans l’absolu, le try/parse pattern est supposé plus performant car il fait moins d’opérations que le TryParse.

Sur ma machine, sur 10.000.000 itérations :

  • La variable input = 007 : la différence est de 50 millisecondes en faveur du try/parse pattern
  • La variable input = a : la différence est de 207.239 millisecondes (plus de 3 minutes) en faveur du tester/doer pattern

Ainsi, l’utilisation que l’on en a va devoir être conditionnée à la source de données.
Si la source est fiable (que les données sont effectivement convertibles), il faudra mieux aller à l’approche optimiste : try/parse. Par contre, si les données sont moins fiables (ex : formulaire web), alors le tester/doer est une meilleure solution.

Bien sûr, le test ici est effectué sur la conversion de string vers int, mais elle est valable pour tous les cas similaires.
Lors du développement d’une API, il est bien vu d’exposer les deux méthodes (pour laisser au choix des utilisateurs de l’API, selon leurs besoins).

 

Découpler gestion d’exceptions et logging

 

Déjà, parce que ce sont deux actions très différentes.
Ensuite, parce que loguer dans le journal d’événements ou dans un fichier texte ou xml, c’est pas la même chose.

Dans du code, j’ai vu que le constructeur même de l’exception custom allait loguer dans le journal d’événements.
Mais au final, ça fait que : 1. le journal est surchargé (une exception pouvant être attrapée et levé avec un « new » à plusieurs reprises), 2. utilisé comme une API, le-dit code n’a pas forcément les droits d’écrire dans le journal (le pendant : l’utilisateur de l’API peut vouloir loguer autre part).

Au final, il est plus pertinent de loguer une seule et unique fois un problème.
Ça sera plus simple de le trouver et on pourra ensuite penser à automatiser la lecture du journal.

 

Autres points

 

Les exceptions sont coûteuses en terme de performance.
Donc les utiliser avec parcimonie peut être une bonne idée.
Un processus de validation des arguments (Code Contracts ?) est une meilleure idée.

Quand on utilise des ressources non managées (fichiers, connexions à une base de données…), il convient d’ajouter un bloc finally pour nettoyer tout ce petit monde.

Quand on développer des API, remonter les « vraies » exceptions et quand même une très bonne idée. Il n’y a rien de plus frustrant pour l’utilisateur d’une API de ne pas savoir ce qu’il se passe (API boîte noire). De même, laisser le choix à l’utilisateur de l’API de comment loguer (prévoir un mécanisme de base pouvant être surchargé, par exemple).

Ne jamais, ô grand jamais, laisser de catch vide.
Déjà, parce que ça va masquer un problème (ce qui est déjà moche), mais qu’en plus il n’y aura jamais rien qui permettra de savoir ce qu’il s’est passé, comment et encore moins de pouvoir le reproduire.

Eviter de catcher la classe Exception directement, préférer les exceptions spécialisées.
Exception est trop générique et ne permettra pas, par exemple, d’obtenir des informations spécifiques portées par des exceptions plus spécifiques (par exemple ArgumentException.ParamName).
Dans le même ordre d’idée, toujours catcher les exceptions que l’applicatif peut gérer et laisser passer les autres.

Il est possible d’utiliser des blocs try/finally sans utiliser de bloc catch.
Cela permet de nettoyer les ressources tout en laissant passer l’exception.

Eviter de mettre un message « Une erreur est survenue » lorsque l’on lance une exception.
Il vaut mieux privilégier un message clair et utile.

Eviter de faire un bloc try avec la Terre entière dedans (principe d’atomicité des méthodes).
Il vaut mieux privilégier des blocs try de petites tailles pour être au plus près de l’exception.

Eviter d’utiliser les exceptions pour le contrôle de flux : DA0007: Avoid using exceptions for control flow.

De préférence, utiliser les exceptions de base du Framework.
Il y a déjà du monde, donc ce n’est pas forcément la peine d’en rajouter.
Pour s’en convaincre, il suffit d’aller voir la hiérarchie d’héritage de SystemException pour cela.
CA1031: Do not catch general exception types.

 

Références

 

Au début, j’ai tenté de les classer. Au début… :)

 

Internet

Best Practices for Exceptions

Creating and Throwing Exceptions (C# Programming Guide)
Exception Handling Fundamentals
Exception Throwing
Using Standard Exception Types
Exceptions and Performance

Comprendre la philosophie des exceptions sous .NET

Guidelines for consistent exception handling
Vexing exceptions
Why are we not to throw these exceptions?
Exception Handling best practices in N-Tier Asp.net applications
Exception Handling
C# .net Exception Handling Best Practice – As Easy as 1, 2, 3?
Exception Handling
How using try catch for exception handling is best practice
Exception Handling Best Practices in .NET
Exceptions and Performance
The evil that exceptions do
Exceptions and Performance Redux
15 Best Practices for Exception Handling

Improve Logging using C# 5.0 Caller Info Attributes(Note: accessibles via Visual Studio 2012 et son nouveau compilateur)

 

Livres conseillés sur Internet

Note: je ne les ai pas lu, mais trié ici par nombre d’avis constatés.

Clean Code: A Handbook of Agile Software Craftsmanship
Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries
Code Complete, Second Edition

 

Conclusion

 

Le sujet est vaste et bien moins évident qu’il n’y parait de prime abord.
Mais cependant, il peut s’avérer critique car si la gestion d’exceptions est mal réalisée, la maintenance corrective et le suivit de production deviendront complexes.
En effet, si l’on ne sait pas quoi chercher ni où chercher, la résolution du problème va être longue et laborieuse.
Non seulement les personnes gérant la maintenance vont s’user plus rapidement, mais cela pourrait éventuellement entraîner des problèmes contractuels (obligation de résolution de bugs sous X heures).

En somme, réfléchir à une bonne gestion d’exception est critique pour une application et doit bien évidemment être traitée en amont.
Sur l’application ayant provoqué le billet, je n’ai pas pu faire de propositions réalistes : trop gros, trop complexe, ce qui entraîne des coûts totalement délirants pour corriger le tir…

About these ads
Catégories:Développement, Divers
  1. Pas encore de commentaire.
  1. 02/06/2014 à 10:01

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

Suivre

Recevez les nouvelles publications par mail.

Rejoignez 56 autres abonnés

%d blogueurs aiment cette page :