Bonnes pratiques pour les exceptions
La gestion appropriée des exceptions est essentielle pour la fiabilité des applications. Vous pouvez gérer intentionnellement des exceptions attendues pour empêcher votre application de se bloquer. Cependant, une application bloquée est plus fiable et plus facile à diagnostiquer qu’une application ayant un comportement non défini.
Cet article présente les meilleures pratiques pour la gestion et la création des exceptions.
Gestion des exceptions
Voici des meilleures pratiques pour gérer les exceptions :
- Utiliser des blocs try/catch/finally pour récupérer après des erreurs ou pour libérer des ressources
- Gérer les conditions courantes pour éviter les exceptions
- Intercepter les exceptions d’annulation et les exceptions asynchrones
- Concevoir les classes de façon à éviter les exceptions
- Restaurer l’état quand des méthodes ne sont pas exécutées en raison d’exceptions
- Capturer et redéclencher les exceptions correctement
Utiliser des blocs try/catch/finally pour procéder à une récupération après des erreurs ou libérer des ressources
Pour le code susceptible de générer une exception et quand votre code application peut récupérer de cette exception, utilisez des blocs try
/catch
autour du code. Dans les blocs catch
, veillez à toujours classer les exceptions de la plus dérivée à la moins dérivée. (Toutes les exceptions dérivent de la classe Exception. Les autres exceptions dérivées ne sont pas gérées par une clause catch
qui est précédée d’une clause catch
pour une classe d’exceptions de base.) Quand votre code ne peut pas récupérer suite à une exception, n’interceptez pas cette exception. Activez des méthodes un peu plus haut dans la pile d’appels pour récupérer si possible.
Nettoyez les ressources qui sont allouées avec des instructions using
ou des blocs finally
. Préférez les instructions using
pour nettoyer automatiquement les ressources quand des exceptions sont levées. Utilisez des blocs finally
pour nettoyer les ressources qui n’implémentent pas IDisposable. Le code dans une clause finally
est presque toujours exécuté même lorsque des exceptions sont levées.
Gérer les conditions courantes pour éviter les exceptions
Pour les conditions susceptibles de se produire mais pouvant déclencher une exception, gérez-les de façon à éviter l’exception. Par exemple, si vous essayez de fermer une connexion déjà fermée, vous obtenez une exception InvalidOperationException
. Vous pouvez l’éviter avec une instruction if
qui permet de vérifier l’état de la connexion avant d’essayer de la fermer.
if (conn.State != ConnectionState.Closed)
{
conn.Close();
}
If conn.State <> ConnectionState.Closed Then
conn.Close()
End IF
Si vous ne vérifiez pas l’état de la connexion avant de la fermer, vous pouvez intercepter l’exception InvalidOperationException
.
try
{
conn.Close();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.GetType().FullName);
Console.WriteLine(ex.Message);
}
Try
conn.Close()
Catch ex As InvalidOperationException
Console.WriteLine(ex.GetType().FullName)
Console.WriteLine(ex.Message)
End Try
Le choix de l’approche dépend de la fréquence à laquelle l’événement doit normalement se produire.
Utilisez la gestion des exceptions si l’événement ne se produit pas souvent, c’est-à-dire, si l’événement est véritablement exceptionnel et indique une erreur, telle qu’une fin de fichier inattendue. Lorsque vous utilisez la gestion des exceptions, la quantité de code exécutée en situation normale est moindre.
Recherchez les conditions d’erreur dans le code si l’événement se produit régulièrement et peut être considéré comme faisant partie de l’exécution normale. Quand vous recherchez les conditions d’erreur courantes, vous exécutez moins de code, car vous évitez les exceptions.
Remarque
Des contrôles effectués en amont éliminent la plupart du temps les exceptions. Cependant, il peut y avoir des conditions de concurrence où la condition surveillée change entre le moment du contrôle et celui de l’opération, et dans ce cas, vous pouvez donc néanmoins être exposé à une exception.
Appeler des méthodes Try*
pour éviter les exceptions
Si le coût en performances des exceptions est prohibitif, certaines méthodes de la bibliothèque .NET offrent d’autres formes de gestion des erreurs. Par exemple, Int32.Parse lève une OverflowException si la valeur à analyser est trop grande pour être représentée par Int32. Cependant, Int32.TryParse ne lève pas cette exception. Au lieu de cela, elle retourne un booléen et a un paramètre out
qui contient l’entier valide analysé en cas de réussite. Dictionary<TKey,TValue>.TryGetValue a un comportement similaire pour tenter d’obtenir une valeur à partir d’un dictionnaire.
Intercepter les exceptions d’annulation et les exceptions asynchrones
Il est préférable d’intercepter OperationCanceledException au lieu de TaskCanceledException, qui dérive de OperationCanceledException
, quand vous appelez une méthode asynchrone. De nombreuses méthodes asynchrones lèvent une exception OperationCanceledException si une annulation est demandée. Ces exceptions permettent d’arrêter efficacement l’exécution et de dérouler l’arborescence des appels de procédure une fois qu’une demande d’annulation est observée.
Les méthodes asynchrones stockent les exceptions qui sont levées pendant l’exécution dans la tâche qu’elles retournent. Si une exception est stockée dans la tâche retournée, cette exception sera levée quand la tâche est attendue. Les exceptions d’utilisation, comme ArgumentException, sont néanmoins levées de façon synchrone. Pour plus d’informations, consultez Exceptions asynchrones.
Concevoir des classes pour éviter les exceptions
Une classe peut fournir des méthodes ou propriétés qui vous permettent d’éviter d’effectuer un appel susceptible de déclencher une exception. Par exemple, la classe FileStream fournit des méthodes qui permettent de déterminer si la fin du fichier a été atteinte. Vous pouvez appeler ces méthodes pour éviter l’exception qui est levée si vous lisez au-delà de la fin du fichier. L’exemple suivant montre comment lire un fichier jusqu’à la fin sans lever d’exception :
class FileRead
{
public static void ReadAll(FileStream fileToRead)
{
ArgumentNullException.ThrowIfNull(fileToRead);
int b;
// Set the stream position to the beginning of the file.
fileToRead.Seek(0, SeekOrigin.Begin);
// Read each byte to the end of the file.
for (int i = 0; i < fileToRead.Length; i++)
{
b = fileToRead.ReadByte();
Console.Write(b.ToString());
// Or do something else with the byte.
}
}
}
Class FileRead
Public Sub ReadAll(fileToRead As FileStream)
' This if statement is optional
' as it is very unlikely that
' the stream would ever be null.
If fileToRead Is Nothing Then
Throw New System.ArgumentNullException()
End If
Dim b As Integer
' Set the stream position to the beginning of the file.
fileToRead.Seek(0, SeekOrigin.Begin)
' Read each byte to the end of the file.
For i As Integer = 0 To fileToRead.Length - 1
b = fileToRead.ReadByte()
Console.Write(b.ToString())
' Or do something else with the byte.
Next i
End Sub
End Class
Un autre moyen d’éviter les exceptions est de retourner null
(ou une valeur par défaut) pour les cas d’erreur les plus courants, au lieu de lever une exception. Un cas d’erreur répandu peut être considéré comme un flux de contrôle normal. En retournant null
(ou une valeur par défaut) dans ces cas-là, vous réduisez l’impact sur les performances d’une application.
Pour les types valeur, déterminez s’il faut utiliser Nullable<T>
ou default
comme indicateur d’erreur pour votre application. À l’aide de Nullable<Guid>
, default
devient null
au lieu de Guid.Empty
. Parfois, l’ajout de Nullable<T>
peut éclaircir les choses, lorsqu’une valeur est présente ou absente. D’autres fois, l’ajout de Nullable<T>
peut créer des cas supplémentaires qui ne sont pas nécessaires et uniquement servir pour créer les sources potentielles d’erreurs.
Restaurer l’état quand les méthodes ne sont pas exécutées en raison d’exceptions
Les appelants doivent supposer qu'il n'y a aucun effet secondaire quand une exception est levée à partir d'une méthode. Par exemple, si vous avez du code qui transfère de l’argent en le retirant d’un compte pour le déposer dans un autre, et qu’une exception est levée pendant l’exécution du transfert, vous ne voulez pas que le retrait reste en vigueur.
public void TransferFunds(Account from, Account to, decimal amount)
{
from.Withdrawal(amount);
// If the deposit fails, the withdrawal shouldn't remain in effect.
to.Deposit(amount);
}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
from.Withdrawal(amount)
' If the deposit fails, the withdrawal shouldn't remain in effect.
[to].Deposit(amount)
End Sub
La méthode précédente ne lève aucune exception directement. Toutefois, vous devez écrire la méthode afin que le retrait soit inversé si l’opération de dépôt échoue.
Pour gérer cette situation, vous pouvez intercepter toutes les exceptions levées par la transaction de dépôt et annuler le retrait.
private static void TransferFunds(Account from, Account to, decimal amount)
{
string withdrawalTrxID = from.Withdrawal(amount);
try
{
to.Deposit(amount);
}
catch
{
from.RollbackTransaction(withdrawalTrxID);
throw;
}
}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
Dim withdrawalTrxID As String = from.Withdrawal(amount)
Try
[to].Deposit(amount)
Catch
from.RollbackTransaction(withdrawalTrxID)
Throw
End Try
End Sub
Cet exemple illustre l’utilisation de throw
pour lever de nouveau l’exception d’origine, ce qui permet aux appelants de voir plus facilement la véritable cause du problème sans avoir à examiner la propriété InnerException. Vous pouvez aussi lever une nouvelle exception et inclure l’exception d’origine comme exception interne.
catch (Exception ex)
{
from.RollbackTransaction(withdrawalTrxID);
throw new TransferFundsException("Withdrawal failed.", innerException: ex)
{
From = from,
To = to,
Amount = amount
};
}
Catch ex As Exception
from.RollbackTransaction(withdrawalTrxID)
Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With
{
.From = from,
.[To] = [to],
.Amount = amount
}
End Try
Capturer et redéclencher les exceptions correctement
Une fois qu’une exception est levée, une partie des informations qu’elle contient est la trace de la pile. La trace de la pile est une liste de la hiérarchie des appels de méthode qui commence par la méthode qui lève l’exception et se termine par la méthode qui intercepte l’exception. Si vous redéclenchez une exception en la spécifiant dans l’instruction throw
, par exemple throw e
, l’arborescence des appels de procédure est redémarrée à la méthode actuelle, et la liste des appels de méthode présents entre la méthode d’origine qui a levé l’exception et la méthode actuelle est perdue. Pour conserver les informations de l’arborescence des appels de procédure d’origine avec l’exception, deux options sont possibles, selon l’endroit où vous redéclenchez l’exception :
- Si vous redéclenchez l’exception depuis le gestionnaire (un bloc
catch
) qui a intercepté l’instance de l’exception, utilisez l’instructionthrow
sans spécifier l’exception. La règle d’analyse du code CA2200 vous aide à trouver les emplacements dans votre code où vous risquez de perdre par inadvertance les informations de l’arborescence des appels de procédure. - Si vous levez à nouveau l’exception à partir d’un autre emplacement que le gestionnaire (bloc
catch
), utilisez ExceptionDispatchInfo.Capture(Exception) pour capturer l’exception dans le gestionnaire et ExceptionDispatchInfo.Throw() quand vous souhaitez la lever à nouveau. Vous pouvez utiliser la propriété ExceptionDispatchInfo.SourceException pour inspecter l’exception capturée.
L’exemple suivant montre comment la classe ExceptionDispatchInfo peut être utilisée et à quoi peut ressembler la sortie.
ExceptionDispatchInfo? edi = null;
try
{
var txt = File.ReadAllText(@"C:\temp\file.txt");
}
catch (FileNotFoundException e)
{
edi = ExceptionDispatchInfo.Capture(e);
}
// ...
Console.WriteLine("I was here.");
if (edi is not null)
edi.Throw();
Si le fichier de l’exemple de code n’existe pas, la sortie suivante est générée :
I was here.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.
File name: 'C:\temp\file.txt'
at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
at System.IO.File.ReadAllText(String path, Encoding encoding)
at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12
--- End of stack trace from previous location ---
at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24
Levée des exceptions
Voici des meilleures pratiques pour lever les exceptions :
- Utiliser des types d’exceptions prédéfinis
- Utiliser des méthodes du générateur d’exceptions
- Inclure une chaîne de message localisée
- Utiliser une grammaire appropriée
- Bien positionner les instructions throw
- Ne pas lever d’exceptions dans des clauses finally
- Ne pas lever d’exceptions depuis des endroits inattendus
- Lever des exceptions de validation d’argument de façon synchrone
Utiliser des types d’exceptions prédéfinis
N'introduisez une nouvelle classe d'exception que quand aucune classe d'exception prédéfinie ne s'applique. Par exemple :
- Si un appel de jeu de propriétés ou de méthode n’est pas approprié étant donné l’état actuel de l’objet, levez une exception InvalidOperationException.
- Si des paramètres non valides sont passés, levez une exception ArgumentException ou l’une des classes prédéfinies qui dérivent de ArgumentException.
Remarque
S’il est préférable d’utiliser des types d’exceptions prédéfinis quand c’est possible, vous ne devez pas déclencher certains types d’exceptions réservés, comme AccessViolationException, IndexOutOfRangeException, NullReferenceException et StackOverflowException. Pour plus d’informations, consultez CA2201 : Ne levez pas des types d’exceptions réservés.
Utiliser des méthodes de générateur d’exceptions
Il est fréquent qu’une classe lève la même exception à partir de différents endroits de son implémentation. Pour éviter d’avoir du code en quantité excessive, utilisez des méthodes d’assistance qui créent l’ exception et la retournent. Par exemple :
class FileReader
{
private readonly string _fileName;
public FileReader(string path)
{
_fileName = path;
}
public byte[] Read(int bytes)
{
byte[] results = FileUtils.ReadFromFile(_fileName, bytes) ?? throw NewFileIOException();
return results;
}
static FileReaderException NewFileIOException()
{
string description = "My NewFileIOException Description";
return new FileReaderException(description);
}
}
Class FileReader
Private fileName As String
Public Sub New(path As String)
fileName = path
End Sub
Public Function Read(bytes As Integer) As Byte()
Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes)
If results Is Nothing
Throw NewFileIOException()
End If
Return results
End Function
Function NewFileIOException() As FileReaderException
Dim description As String = "My NewFileIOException Description"
Return New FileReaderException(description)
End Function
End Class
Certains types d’exceptions .NET clés ont des méthodes d’assistance throw
statiques qui allouent et lèvent l’exception. Vous devez appeler ces méthodes au lieu de construire et de lever le type d’exception correspondant :
- ArgumentNullException.ThrowIfNull
- ArgumentException.ThrowIfNullOrEmpty(String, String)
- ArgumentException.ThrowIfNullOrWhiteSpace(String, String)
- ArgumentOutOfRangeException.ThrowIfZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfNegative<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNotEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThanOrEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<T>(T, T, String)
- ObjectDisposedException.ThrowIf
Conseil
Les règles d’analyse du code suivantes peuvent vous aider à trouver les emplacements dans votre code où vous pouvez tirer parti de ces assistances throw
statiques : CA1510, CA1511, CA1512 et CA1513.
Si vous implémentez une méthode asynchrone, appelez CancellationToken.ThrowIfCancellationRequested() au lieu de vérifier si une annulation a été demandée, puis construisez et levez OperationCanceledException. Pour plus d’informations, consultez CA2250.
Inclure une chaîne de message localisée
Le message d’erreur que l’utilisateur voit est dérivé de la propriété Exception.Message de l’exception qui a été levée, et non pas du nom de la classe d’exception. En règle générale, vous affectez une valeur à la propriété Exception.Message en passant la chaîne de message à l’argument message
d’un constructeur d’exception.
Pour les applications localisées, vous devez fournir une chaîne de message localisée pour chaque exception que votre application peut lever. Vous utilisez des fichiers de ressources pour fournir les messages d’erreur localisés. Pour plus d’informations sur la localisation d’applications et la récupération des chaînes localisées, consultez les articles suivants :
- Guide pratique : créer des exceptions définies par l’utilisateur avec des messages d’exception localisés
- Ressources dans les applications .NET
- System.Resources.ResourceManager
Utiliser une grammaire appropriée
Écrivez des phrases claires et insérez une ponctuation finale. Chaque phrase de la chaîne affectée à la propriété Exception.Message doit se terminer par un point. Par exemple, « La table du journal a débordé » utilise une grammaire et une ponctuation correctes.
Bien positionner les instructions throw
Placez les instructions throw là où l’arborescence des appels de procédure sera utile. La trace de la pile commence à l'instruction où l'exception est levée et se termine à l'instruction catch
qui intercepte l'exception.
Ne pas lever d’exceptions dans des clauses finally
Ne levez pas d’exceptions dans des clauses finally
. Pour plus d’informations, consultez la règle d’analyse du code CA2219.
Ne pas lever d’exceptions depuis des endroits inattendus
Certaines méthodes, comme Equals
, GetHashCode
et ToString
, les constructeurs statiques et les opérateurs d’égalité ne doivent pas lever d’exceptions. Pour plus d’informations, consultez la règle d’analyse du code CA1065.
Lever des exceptions de validation d’argument de façon synchrone
Dans les méthodes retournant des tâches, vous devez valider les arguments et lever les exceptions correspondantes, comme ArgumentException et ArgumentNullException, avant d’entrer dans les parties asynchrones de la méthode. Les exceptions qui sont levées dans la partie asynchrone de la méthode sont stockées dans la tâche retournée et n’apparaissent pas avant que (par exemple) la tâche soit attendue. Pour plus d’informations, consultez Exceptions dans les méthodes retournant des tâches.
Types d’exceptions personnalisés
Voici des meilleures pratiques pour les types d’exceptions personnalisés :
- Terminer les noms des classes d’exceptions par
Exception
- Inclure trois constructeurs
- Fournir des propriétés supplémentaires selon les besoins
Terminer les noms des classes d’exceptions par Exception
Quand une exception personnalisée est nécessaire, nommez-la de manière appropriée et dérivez-la de la classe Exception. Par exemple :
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
Inherits Exception
End Class
Inclure trois constructeurs
Utilisez au moins les trois constructeurs communs pendant la création de vos propres classes d’exception : le constructeur sans paramètre, un constructeur qui prend un message de type chaîne et un constructeur qui prend un message de type chaîne et une exception interne.
- Exception(), qui utilise les valeurs par défaut.
- Exception(String), qui accepte un message de type chaîne.
- Exception(String, Exception), qui accepte un message de type chaîne et une exception interne.
Pour obtenir un exemple, consultez Guide pratique : Créer des exceptions définies par l’utilisateur.
Fournir des propriétés supplémentaires selon les besoins
Spécifiez des propriétés supplémentaires (en plus de la chaîne de message personnalisée) pour une exception seulement dans le cas d’un scénario du programme où les informations supplémentaires sont utiles. Par exemple, la classe FileNotFoundException fournit la propriété FileName.