Gestion des erreurs sur la plateforme .NET Par Sébastien Andreo  

La gestion des erreurs! On entend ce terme fréquemment, il est souvent accompagné d'autres mots tels que exception, code de retour, erreur, handler, etc. Je me propose dans cette article de clarifier ce vocabulaire et de présenter certaines règles d'utilisation des mécanismes de gestion d'erreur de la plateforme .Net. Nous verrons aussi comment mettre en place une gestion d'erreur générique.

errière le terme de "gestion des erreurs" se cache souvent un spectre beaucoup plus large que le simple fait de gérer une erreur. En effet, il faut :

  • identifier ce qu'est une erreur d'exécution

  • définir comment la transporter

  • définir pour qui cette erreur est significative

  • définir comment la traiter

Dans cette article nous allons présenter chacun de ces aspects et les appliquer à la plateforme .Net.

Qu'est ce qu'une erreur d'exécution?

La définition d'une erreur d'exécution n'est bien évidement pas liée à la plateforme .Net, Java EE ou autre et donner une définition n'est pas chose simple. Prenons l'exemple d'une ouverture de connexion TCP/IP avec un serveur dont la connexion échoue. Ceci doit être considéré comme une erreur d'exécution au niveau du protocole. Par contre si l'on invoque sur un serveur une liste de services et qu'aucun n'est supporté par le serveur il n'y a pas d'erreur d'exécution (enfin du moins au niveau du protocole).

Donc pour résumer il n'y a erreur que lorsque le contrat "moral" entre les 2 partenaires est brisé. Ou encore, une erreur d'exécution se produit lorsqu'un partenaire ne peut pas réaliser ce pour quoi il a été développé.

Transporter les erreurs

Il y a 2 méthodes pour transporter les erreurs, les codes de retour ou les exceptions. La plateforme .Net est clairement orientée exception. Mais pourquoi ? Et surtout comment utiliser ce mode de transport le plus efficacement possible.

Intérêt des exceptions

Voici donc une liste non exhaustive de raisons de choisir de transporter les erreurs par le biais d'exceptions:

  • L'exception favorise les APIs dites consistantes, car celles-ci ne sont utilisées (et surtout ne doivent être utilisées) que pour transporter une erreur. A contrario, un code de retour peut rentre compte d'une multitude de choses (Erreur, warning, nombre d'octets reçus).
  • L'exception peut être utilisée dans tous les cas de figure: méthode, propriété ou constructeur.
  • L'exception permet de rendre le code plus robuste car il est très simple d'ignorer un code de retour, alors qu'ignorer une exception est beaucoup plus complexe. D'ailleurs il ne faut pas attraper une exception si l'on ne sait pas comment la traiter. Si l'exception remonte jusqu'au runtime alors il y a un bug dans l'application, on pourra l'identifier, le corriger et rendre le système encore plus robuste.
  • On sépare le code métier du code du traitement d'erreur (block catch) ceci rendant par la même le code métier beaucoup plus lisible.
  • L'exception peut transporter des informations très complètes sur la cause de l'erreur.
  • Et puis chose très agréable lors du lancement d'une exception le debugger peut directement pointer sur la ligne de code d'où l'exception a été lancée.

Clairement l'utilisation d'exceptions pour transporter les erreurs procure beaucoup d'avantages. Mais il faut également utiliser se mécanisme correctement voici quelques guidelines.

Guidelines

1) Toutes les exceptions d'une API publique doivent être documentées.

2) Il ne faut pas différencier les exceptions sur un de leurs membres (par exemple : je fais ceci si le Message d'erreur commence par "Z"). Il faut éviter de lancer une exception explicitement dans un block finally.

3) Une bonne pratique consiste aussi à utiliser en priorité les exceptions fournies par le framework lorsque celles-ci correspondent à l'erreur qui doit être transportée.

4) On doit toujours envoyer une exception spécifique (par exemple: on lance ArgumentNullException plutôt que ArgumentException) plutôt qu'une classe générique.

5) Il ne faut pas non plus utiliser de block catch sur des exceptions non spécifique (ex: catch(System.Exception e){...}). Lorsque l'on attrape une exception il faut exactement savoir ce que l'on va en faire car le catch à un coût. De plus, en attrapant System.Exception on aura perdu toute la sémantique de l'erreur. On sait juste que quelque chose s'est mal passé.

6) Utiliser un block try finally lorsque l'on veut libérer des ressources mais sans chercher à traiter l'erreur.

FileStream stream = null;
try{
stream = new FileStream(...);
}
finally{
if(steam!=null)stream.Close();
}

Il n'y a là aucun problème de performance.

7) Ne pas lancer explicitement les exceptions de type StackOverFlowException, OutOfMemoryException, ComException, ExecutionEngineException,... Ces exceptions ne sont lancées que par l'infrastructure du CLR.

8) Ne pas permettre à une API publique de lancer des exceptions du type NullreferenceException, IndexOutOfRangeException, AccessViolationException. Pour cela il suffit de vérifier la validité des arguments et ensuite de renvoyer une exception de type ArgumentNullException ou ArgumentOutOfRangeException.

9) Lorsqu'un objet est dans un état invalide renvoyer InvalidOperationException.

10) Il peut arriver que les exceptions disponibles ne soit pas utilisables il faut donc en créer de nouvelles. Et bien évidement la aussi quelques règles doivent être respectées.

11) Éviter des hiérarchies très ou trop profondes.

12) Toujours se dériver de System.Exception ou d'une des autres exceptions de base.

13) Le nom de la classe doit toujours se terminer par le suffixe Exception.

14) Une Exception doit être sérialisable.

15) Une exception doit au moins proposer ces constructeur:

public class SomeException
: Exception, ISerializable{
public SomeException();
public SomeException(string message);
public SomeException(string message, Exception inner);
// this constructor is needed for serialization
protected SomeException(SerializationInfo info, StreamingContext ctx);
}

Les exceptions présentent beaucoup d'avantages mais également un gros inconvénient, les performances chutent lors du "lancé" d'exception. Vous me direz lorsqu'une exception est lancée on se trouve dans un cas d'erreur, et le faite de perdre en performance est raisonnablement acceptable. Cependant, pour certaines applications, ceci n'est pas acceptable. Imaginons le transfert d'un gros volume de données par le réseau; vous n'allez pas ralentir ou pire annuler le transfert parce que quelqu'un n'a pas écrit la date correctement. Dans ce cas on peut utiliser le Try-Parse pattern; DateTime fournit un exemple d'implémentation.

Traiter l'erreur

On a vu précédemment que l'utilisation d'exception permettait de séparer le code métier du code de traitement de l'erreur. Pour réaliser cette séparation on utilise un bock try catch. Mais il peut arriver que le même traitement d'erreur se répète à plusieurs endroits dans le code. Ou encore, on souhaite lors de toutes les phases de traitement d'erreur (block catch) effectuer une opération tel que laisser une trace dans un log ou écrire dans l'event viewer...

La mauvaise méthode pour implémenter ces traitements serait d'utiliser le copier coller (d'ailleurs en passant le copier coller est toujours la mauvaise méthode). Il faut aussi se rappeler d'une règle de la programmation objet: lorsqu'une classe doit changer, elle ne doit changer que pour une seule raison. Alors si notre code métier change pourquoi devrait-on changer notre traitement d'erreur? La réponse est simple parce que le traitement d'erreur fait partie intégrante du code métier (même s'il est localisé dans un block catch). Il nous faudrait une infrastructure qui externaliserait complètement ce code de traitement d'erreur et qui plus est soit une infrastructure générique donc réutilisable.

Externaliser le traitement en utilisant une infrastructure générique

L'idée de base est de pouvoir définir dans des objets un traitement générique de base par exemple un rollback sur une session DB, écrire une trace, logger, relancer une exception avec plus de contexte. Et de pouvoir les agréger afin de construire notre traitement d'erreur (par exemple : rollBack + trace). Pour cela on va définir une interface dont voici le code:

public interface IErrorHandlingStrategy <ExceptionType> {
void Handle(ExceptionType _exception_in);
}

Cette interface utilise les Generics de .Net 2.0 pour typer l'exception qui sera à traiter. On est maintenant capable d'implémenter nos traitements mais il nous faut quelqu'un pour les exécuter. C'est le rôle de l'ExceptionHandler dont voici le code:

 
public class ExceptionHandler <ExceptionType, ErrorHandlingStrategyType>
where ErrorHandlingStrategyType : IErrorHandlingStrategy <ExceptionType>, new() {
private ErrorHandlingStrategyType m_strategy;
public ExceptionHandler() {
m_strategy = new ErrorHandlingStrategyType();
}

public void Handle(ExceptionType _exception_in) {
m_strategy.Handle(_exception_in);
}
}

Lui aussi utilise les Generics, lors de son instantiation on lui passera l'exception qu'il aura à traiter et la manière de la traiter on obtiendra ainsi un code ressemblant à celui-ci:

 
try{
// something throws an exception of type IOException
}
catch(IOException ex){
ExceptionHandler hdler = new ExceptionHandler < IOException,TraceAndReThrowStrategy >();
hdler.handle(ex);
}

Avec cette infrastructure, notre code métier ne sera pas impacté par un changement de notre gestion d'erreur. On pourra factoriser et réutiliser certains traitements et tout cela dans un environnement typesafe.

Et Pourquoi pas un peu d'AOP ?

Certains me diront mais on peut encore aller plus loin dans l'externalisation du traitement de l'erreur... Et oui pourquoi pas appeler notre tisseur préferé pour rajouter le traitement de l'erreur là où bon nous semble? Ainsi le développeur vivrait dans un monde parfait, sans erreurs, et seulement après coup; grâce à une bonne documentation des erreurs on pourrait rajouter le traitement en lieu et place et faire évoluer ce traitement au cours du temps sans jamais impacter le code métier qui, je le rappelle est ce pourquoi nous sommes rémunéré...

Conclusion

J'espère avoir démontré l'intérêt d'utiliser les exceptions pour transporter les erreurs d'exécution plutôt que les codes de retour. Cependant pour réaliser un error handling correct il ne suffit pas de lancer des exceptions, il faut suivre un certain nombre de règles et il faut globalement répondre à 3 questions:

  • Quoi? : Est ce une erreur d'exécution
  • Pour qui? : à qui est destinée cette erreur
  • Où? : Ou traiter cette erreur
     

Il faut également noter que le traitement des erreurs n'est pas quelque chose de naturel pendant une phase de développement. Et que pouvoir mettre en place des traitements d'erreurs génériques ou, encore mieux, les tisser une fois l'implémentation fonctionnelle réalisée changera radicalement la manière de développer.


Framework Design Guidelines (Krysztof Cwalina & Brad Abrams) : ISBN 0-321-24675-6