Quand quelque chose tourne mal à l'exécution (un fichier manque, un texte n'est pas un nombre, une clé n'est pas dans un dictionnaire), .NET lève une exception : un objet qui décrit l'erreur. L'exception remonte la pile d'appels jusqu'à ce qu'un bloc catch la traite. Si rien ne la traite, le programme s'arrête et affiche l'erreur.
Un try catch simple
Enveloppez le code qui peut échouer dans try, et traitez l'échec dans catch :
Sortie :
42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running
Quand int.Parse("forty-two") lève une exception, le Console.WriteLine qui le suit dans le bloc try est sauté, le bloc catch (FormatException) s'exécute, et la boucle continue. Vous pouvez omettre la variable (catch (FormatException)) quand vous n'avez pas besoin de l'objet exception.
Ce cas précis a un meilleur outil : int.TryParse(input, out int n) renvoie false au lieu de lever une exception. Les exceptions servent aux situations que le code n'attend pas ; une saisie souvent invalide est attendue, donc vérifiez-la au lieu d'intercepter.
Comment une exception remonte
Une exception levée au fond d'une chaîne d'appels déroule chaque méthode jusqu'à trouver un gestionnaire correspondant. Le code qui suit le point de levée dans chacune de ces méthodes ne s'exécute pas.
Sortie :
OrderTotal finished
5.00
Caught KeyNotFoundException in Main
PriceOf et OrderTotal n'ont pas de catch, donc l'exception les traverse directement jusqu'à Main. Placez un gestionnaire au niveau qui sait quoi faire de l'échec, ce qui n'est souvent pas là où il s'est produit.
Intercepter des exceptions précises, dans le bon ordre
Une clause catch traite son type et tous les types qui en dérivent. Quand il y a plusieurs clauses, la première qui correspond l'emporte, donc classez-les du plus spécifique au plus général :
Sortie :
10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException
Le dernier appel lève OverflowException, qu'aucune clause spécifique ne traite, donc c'est le catch (Exception e) général qui s'en charge. Placer catch (Exception) en premier rendrait les clauses suivantes inaccessibles, et le compilateur le refuse avec l'erreur CS0160.
Filtres d'exception avec when
C# 6 a ajouté when : une condition qui décide si une clause catch s'applique. Si elle vaut false, le runtime continue de chercher un autre gestionnaire comme si la clause n'existait pas.
Sortie :
Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401
Les filtres permettent de choisir une branche selon des données contenues dans l'exception sans intercepter puis relancer. Ils sont aussi la façon propre de traiter de manière identique deux types sans rapport, comme le fait la troisième clause. Un filtre s'exécute avant le déroulement de la pile, donc un débogueur ou un vidage mémoire après plantage montre encore l'état d'origine quand aucun filtre ne correspond.
finally : du code qui s'exécute toujours
Un bloc finally s'exécute quand le contrôle quitte le try, qu'il se soit terminé, soit revenu plus tôt ou ait levé une exception. C'est là que va le nettoyage. (Une réserve : si rien n'intercepte l'exception nulle part, le processus peut se terminer sans l'exécuter.)
Sortie :
Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error
Dans tous les cas, « Close connection » s'affiche avant que le résultat de la méthode n'atteigne Main : le finally s'exécute après le calcul de la valeur du return, mais avant que la méthode ne revienne réellement. Un try peut avoir un finally sans aucun catch, ce qui nettoie tout en laissant l'exception continuer vers l'appelant.
Pour les objets qui implémentent IDisposable (fichiers, flux, connexions), l'instruction using écrit ce try/finally pour vous.
Relancer : throw; ou throw e;
Parfois, un bloc catch journalise ou enregistre quelque chose puis laisse l'exception continuer. La façon de relancer détermine si la trace de pile survit :
Sortie :
throw; trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False
throw e; traite l'exception comme nouvellement levée depuis le bloc catch, donc les cadres situés en dessous, y compris la méthode où l'erreur s'est produite, disparaissent de la trace. Relancez toujours avec un simple throw;. (L'attribut NoInlining n'est là que parce que le JIT pourrait fusionner une méthode aussi petite avec son appelant, ce qui la cacherait des deux traces.)
Pour ajouter du contexte, enveloppez plutôt l'exception dans une nouvelle et passez l'originale comme exception interne : throw new ConfigException("Could not start the app", e);. L'exception interne garde sa propre trace de pile, et les outils de journalisation affichent la chaîne. Écrire vos propres types d'exception est présenté dans lever des exceptions.
L'objet Exception
Toute exception dérive de System.Exception. Les membres que vous utiliserez le plus :
| Membre | Ce qu'il contient |
|---|---|
Message | Une description lisible par un humain |
GetType().Name | Le type d'exception, comme FormatException |
StackTrace | La chaîne d'appels de méthodes au point de levée |
InnerException | L'exception qui a causé celle-ci, ou null |
ToString() | Type, message, exceptions internes et trace de pile réunis |
Journalisez e.ToString() plutôt que e.Message quand vous voulez diagnostiquer un échec plus tard : le message seul dit rarement où était le problème. Ne montrez pas e.ToString() aux utilisateurs finaux.
Types d'exception courants
| Exception | Cause typique |
|---|---|
NullReferenceException | Appeler un membre sur une référence null |
ArgumentNullException | Une méthode a reçu null là où il lui faut une valeur |
ArgumentOutOfRangeException | Un argument ou un index de liste est hors de la plage autorisée |
IndexOutOfRangeException | Un index de tableau est hors de ses limites |
FormatException | int.Parse, DateTime.Parse et consorts sur un texte au mauvais format |
InvalidCastException | Un cast explicite vers un type que l'objet n'est pas |
InvalidOperationException | L'objet n'est pas dans le bon état pour l'appel (séquence vide, collection modifiée) |
KeyNotFoundException | Lire avec l'indexeur une clé absente d'un dictionnaire |
DivideByZeroException | Division entière ou decimal par zéro |
OverflowException | Un nombre analysé, une conversion checked ou un résultat arithmétique checked ne tient pas dans le type |
FileNotFoundException, IOException | Problèmes de système de fichiers |
NullReferenceException, IndexOutOfRangeException et InvalidCastException signalent presque toujours un bug. Corrigez le code plutôt que de les intercepter.
Ne pas avaler les exceptions
Un catch vide cache toutes les erreurs, y compris celles que vous n'aviez pas prévues :
try
{
SaveOrder(order);
}
catch (Exception)
{
// nothing: the order silently was not saved
}
Le programme continue comme si l'enregistrement avait fonctionné, et la vraie cause est perdue. Des règles qui gardent la gestion des exceptions honnête :
- N'interceptez que ce que vous pouvez traiter, au niveau qui peut le traiter.
- Si vous interceptez pour journaliser, relancez avec
throw;, sauf si le programme peut vraiment continuer. - N'interceptez
Exceptionqu'à la limite extérieure :Main, un gestionnaire de requêtes, une boucle de travail. - Préférez
TryParse,TryGetValueet les tests denullaux exceptions pour les cas attendus. Lever une exception est lent comparé à un test, alors qu'un bloctryqui ne lève rien ne coûte presque rien.
Erreurs courantes
catch (Exception)en premier. Les clauses suivantes deviennent inaccessibles (CS0160).throw e;pour relancer. Cela efface la trace de pile d'origine ; utilisezthrow;.- Des blocs catch vides. Les erreurs disparaissent ; au minimum, journalisez et relancez.
- Utiliser les exceptions pour le flux de contrôle. Validez la saisie avec
TryParseau lieu d'intercepterFormatException. - Montrer aux utilisateurs le
e.Messaged'une exception du framework. Sa formulation change d'une version de .NET à l'autre et elle est écrite pour les développeurs.
Questions fréquentes
Comment fonctionne try catch en C# ?
Le code susceptible d'échouer va dans le bloc try. Si une instruction y lève une exception, le reste du bloc est sauté et le runtime cherche une clause catch dont le type correspond à l'exception, d'abord dans la méthode courante puis dans chaque appelant. Le premier catch correspondant s'exécute, et l'exécution reprend après toute l'instruction try.
finally s'exécute-t-il toujours en C# ?
Presque toujours : après que le bloc try se termine normalement, après qu'un catch a traité une exception, après un return ou un break dans le bloc, et quand une exception le traverse en remontant vers un catch plus haut dans la pile d'appels. Il ne s'exécute pas quand le processus se termine avant : un processus tué, Environment.FailFast, une StackOverflowException, et, sur .NET Core et plus, une exception que rien n'intercepte, qui termine le processus avant l'exécution des blocs finally.
Comment intercepter plusieurs exceptions en C# ?
Écrivez plusieurs clauses catch, le type le plus spécifique en premier : catch (FileNotFoundException) avant catch (IOException) avant catch (Exception). Le compilateur rejette une clause qui ne peut jamais être atteinte parce qu'une clause précédente intercepte déjà son type. Pour traiter de la même façon deux types sans rapport, utilisez un filtre : catch (Exception e) when (e is FormatException || e is OverflowException).
Quelle est la différence entre throw et throw ex en C# ?
Dans un catch, throw; relance l'exception courante avec sa trace de pile d'origine. throw ex; lève le même objet mais réinitialise la trace de pile à la ligne courante, donc la méthode où l'erreur s'est réellement produite disparaît de la trace. Utilisez throw;, ou enveloppez l'exception : throw new MyException("context", ex);.
Qu'est-ce qu'un filtre d'exception en C# ?
Une clause when après un catch (C# 6 et plus) : catch (HttpRequestException e) when (e.Message.Contains("404")). Le bloc catch ne s'exécute que si la condition vaut true ; sinon l'exception continue de chercher un autre gestionnaire comme si la clause n'existait pas, et sa pile n'est pas déroulée.
Faut-il intercepter Exception en C# ?
Seulement aux limites d'un programme : le haut de Main, un gestionnaire de requêtes ou une boucle en arrière-plan, où le rôle est de journaliser l'erreur puis de continuer ou de s'arrêter proprement. En profondeur dans le code, interceptez les types précis que vous pouvez réellement traiter, et laissez tout le reste se propager.