Menu

Lever une exception en C# : clauses de garde, expressions throw, exceptions personnalisées

Comment et quand lever des exceptions en C#. Apprenez l'instruction throw, quel type d'exception intégré correspond à quelle erreur, les expressions throw avec ?? et ?:, les fonctions ThrowIfNull, et comment écrire une classe d'exception personnalisée avec ses propres données et une exception interne.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Intercepter les exceptions n'est que la moitié de la gestion des erreurs ; l'autre moitié consiste à lever la bonne. Une exception bien choisie dit exactement à l'appelant ce qui s'est mal passé, et si l'erreur vient de lui ou de l'état du programme. Cette page couvre le côté levée ; try catch couvre le traitement.

L'instruction throw et les clauses de garde

throw prend un objet exception. L'exécution de la méthode s'arrête là, et l'exception cherche un gestionnaire en remontant la pile d'appels. L'usage le plus courant est la clause de garde : des vérifications en tête de méthode qui rejettent une entrée invalide avant tout travail.

Sortie :

ArgumentOutOfRangeException for parameter 'amount'
InvalidOperationException: The account is frozen.
Balance: 100

nameof(amount) produit la chaîne "amount" et reste correct si le paramètre est renommé. Les exceptions d'argument la stockent dans ParamName, que les outils et les logs utilisent pour désigner le mauvais argument.

Les clauses de garde gardent le reste de la méthode simple : une fois les vérifications passées, le code peut supposer une entrée valide. Elles échouent aussi à l'endroit de l'erreur, au lieu de laisser une mauvaise valeur circuler et provoquer trois méthodes plus loin une NullReferenceException déroutante.

Quel type d'exception lever

Réutilisez un type intégré quand il décrit la situation ; les appelants savent déjà le traiter.

SituationLever
Un argument obligatoire vaut nullArgumentNullException
Un argument est hors de la plage autorisée (quantité négative, index au-delà de la fin)ArgumentOutOfRangeException
Un argument est invalide d'une autre façon (nom vide, identifiant mal formé)ArgumentException
L'appel n'est pas valide dans l'état actuel de l'objetInvalidOperationException
L'opération n'est jamais prise en charge par ce type (le Add d'une collection en lecture seule)NotSupportedException
La méthode n'a pas encore été écriteNotImplementedException
Un objet a été utilisé après DisposeObjectDisposedException
Une opération minutée a dépassé son délaiTimeoutException

La frontière entre les trois premières lignes et InvalidOperationException tient à qui doit changer quelque chose. Une exception d'argument dit « appelez ceci autrement ». InvalidOperationException dit « l'appel était correct, mais pas maintenant ».

Ne levez pas directement Exception, SystemException ou ApplicationException : les appelants ne peuvent pas les intercepter sans intercepter aussi tout le reste. Ne levez pas non plus vous-même NullReferenceException, IndexOutOfRangeException ou StackOverflowException ; le runtime les réserve aux vrais bugs.

Expressions throw

Avant C# 7, throw n'était qu'une instruction. Depuis C# 7, il peut aussi apparaître comme expression à trois endroits, ce qui ramène des vérifications courantes à une seule ligne :

Sortie :

Ana <ana@example.com>
Null: name
ArgumentException: email

Notez que ArgumentNullException dérive de ArgumentException, donc un catch (ArgumentException) placé en premier intercepterait aussi le cas null. L'ordre des clauses catch compte pour la même raison quand vous traitez les deux.

ThrowIfNull et ses semblables (.NET 6 et plus)

Le .NET moderne ajoute des fonctions statiques qui écrivent pour vous la vérification et la levée, avec le nom du paramètre capturé automatiquement :

public void Ship(Order order, int quantity, string address)
{
    ArgumentNullException.ThrowIfNull(order);                   // .NET 6
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // .NET 8
    ArgumentException.ThrowIfNullOrWhiteSpace(address);          // .NET 8
    ObjectDisposedException.ThrowIf(disposed, this);             // .NET 7

    // ...
}

Elles se comportent comme le if suivi de throw écrit à la main, et gardent chaque clause de garde sur une ligne. Sur des cibles plus anciennes, écrivez la forme if présentée plus haut.

Écrire une classe d'exception personnalisée

Créez votre propre type d'exception quand les appelants doivent intercepter cet échec précis à part, ou quand le gestionnaire a besoin de données qu'une chaîne de message transporte mal.

Sortie :

Cannot withdraw 25 from a balance of 15.
Short by 10

Les conventions :

  • Le nom se termine par Exception.
  • La classe dérive de Exception (ou d'un type intégré plus spécifique quand il s'agit d'un cas particulier de celui-ci, comme InvalidOperationException).
  • Elle a les trois constructeurs standard : sans argument, avec un message, et avec un message plus une exception interne. Ajoutez vos propres constructeurs par-dessus.
  • Les données supplémentaires vont dans des propriétés en lecture seule, définies dans le constructeur. Un gestionnaire peut alors agir sur e.Requested au lieu d'analyser le message.

Envelopper avec une exception interne

Quand un échec de bas niveau doit apparaître comme un échec de plus haut niveau, enveloppez-le. L'original est conservé comme InnerException, donc aucune information n'est perdue :

Sortie :

Setting 'port' must be a number, got '80a'.
Caused by: FormatException

L'appelant raisonne maintenant en termes de configuration, qu'il comprend, et journaliser e.ToString() affiche toute la chaîne, y compris la FormatException et sa trace de pile. N'enveloppez que si vous ajoutez du sens ; envelopper chaque exception dans une MyAppException générique oblige seulement les gestionnaires à fouiller dans InnerException.

Lever une exception ou renvoyer un résultat

Les exceptions servent aux échecs que l'appelant n'attend pas en fonctionnement normal. Pour les issues courantes, comme une recherche qui ne trouve souvent rien ou une saisie utilisateur souvent invalide, la convention .NET est le schéma Try : renvoyer un bool et transmettre la valeur via un paramètre out.

public bool TryWithdraw(decimal amount, out string error)
{
    if (amount > Balance) { error = "Insufficient funds."; return false; }
    Balance -= amount;
    error = null;
    return true;
}

Beaucoup de types proposent les deux : int.Parse lève une exception, int.TryParse renvoie false ; dict[key] lève une exception, dict.TryGetValue renvoie false. Lever une exception coûte bien plus cher que renvoyer une valeur, elle ne doit donc pas se trouver sur un chemin exécuté des milliers de fois par seconde. Voir ref et out pour les paramètres out.

Écrire de bons messages

Un message d'exception est lu par un développeur qui consulte un log. Faites-lui dire ce qui n'allait pas et, quand c'est sans risque, la valeur fautive : « Quantity must be between 1 and 99, got 0. » vaut mieux que « Invalid input. ». Écrivez des phrases complètes, et gardez les secrets comme les mots de passe et les jetons hors des messages, puisqu'ils finissent dans des fichiers de log.

Erreurs courantes

  • Lever Exception elle-même. Les appelants ne peuvent pas l'intercepter de façon sélective ; utilisez un type spécifique.
  • Passer le message là où va le nom du paramètre. new ArgumentNullException("name") prend le nom du paramètre ; le message vient en second.
  • Des noms de paramètres écrits en dur. Utilisez nameof(param) pour qu'ils restent corrects après un renommage.
  • Des exceptions personnalisées sans signification supplémentaire. Si un type intégré convient, utilisez-le.
  • Perdre l'erreur d'origine en enveloppant. Passez-la toujours comme exception interne.

Questions fréquentes

Comment lever une exception en C# ?

Créez un objet exception et levez-le : throw new ArgumentException("Amount must be positive", nameof(amount));. L'exécution s'arrête à cette ligne et l'exception remonte la pile d'appels jusqu'au catch correspondant le plus proche. Choisissez le type intégré le plus spécifique qui décrit le problème, ou un type personnalisé quand les appelants doivent traiter ce cas à part.

Comment créer une exception personnalisée en C# ?

Dérivez de Exception une classe dont le nom se termine par Exception, et donnez-lui les constructeurs standard : un sans argument, un qui prend un message, et un qui prend un message et une exception interne, chacun appelant le constructeur base(...) correspondant. Ajoutez des propriétés en lecture seule pour toute donnée dont un gestionnaire a besoin, comme un identifiant de commande ou un solde.

Quand lever ArgumentException plutôt qu'InvalidOperationException ?

Levez une ArgumentException (ou ArgumentNullException / ArgumentOutOfRangeException) quand un appelant a passé une mauvaise valeur : la correction consiste à appeler la méthode autrement. Levez InvalidOperationException quand les arguments sont corrects mais que l'objet n'est pas dans le bon état pour l'appel, comme lire depuis une connexion fermée ou retirer de l'argent d'un compte gelé.

Qu'est-ce qu'une expression throw en C# ?

Depuis C# 7, throw peut s'utiliser comme expression à trois endroits : après ??, comme l'une ou l'autre branche de ?:, et comme corps d'un membre à corps d'expression ou d'une lambda. Par exemple, _name = name ?? throw new ArgumentNullException(nameof(name)); affecte ou lève une exception en une ligne.

Que fait ArgumentNullException.ThrowIfNull ?

C'est une fonction statique ajoutée dans .NET 6 : ArgumentNullException.ThrowIfNull(customer); lève ArgumentNullException avec le nom du paramètre renseigné automatiquement quand customer vaut null, et ne fait rien sinon. Les versions suivantes ont ajouté des fonctions similaires comme ArgumentException.ThrowIfNullOrEmpty (.NET 7) et ArgumentOutOfRangeException.ThrowIfNegative (.NET 8).

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER