LE 10/09/2025 — 7 MINUTES DE LECTURE

Faut-il utiliser les exceptions en .NET

Dans la communauté, elles ne laissent personne indifférent. On les accuse de tous les maux, de plomber les performances et la lisibilité du code. Mais est-ce vraiment le cas ?

Faut-il utiliser les exceptions en .NET

Quand j’ai commencé à coder, la gestion des erreurs m’apparaissait comme un vrai casse-tête. Fallait-il retourner null ? Une chaîne vide ? Un tableau vide ? Une exception ? Un objet de résultat ? Impossible de trouver un consensus clair sur le web, et aucun formateur n’a su m’apporter une véritable réponse. 

Comme souvent en programmation : ça dépend. Je déteste cette réponse, même si elle est souvent vraie. Avec l'expérience, c'est devenu plus facile de faire un choix.

 

C'est quoi une exception ? 

Une exception une erreur rencontrée à l’exécution. Il faut l’imaginer comme une bulle qui remonte les différentes couches de votre code jusqu’à être capturée et gérée (ou pas). C’est un mécanisme de défense qui empêche l’application de s'executer dans un état instable ou dégradé.

Dans .NET, elles sont omniprésentes : du runtime aux classes standards, elles sont le mécanisme principal pour gérer les erreurs. Il faudra apprendre à les gérer, sous peine de voir l'application se terminer brutalement.

Pourtant, dans la communauté, elles font débat : on les accuse d’être lentes, trop souvent utilisées pour du contrôle de flux ou de compliquer la lecture du code. À tel point que certains en viennent à les bannir purement et simplement, au profit par exemple du Result Pattern (j’y reviens plus bas).

 

Les recommandations de Microsoft

Une phrase revient souvent : Les exceptions doivent être utilisées pour gérer des cas... exceptionnels. C'est la recommandation officielle de Microsoft dans sa page destinée aux bonnes pratiques :

« Utilisez la gestion des exceptions si l’événement ne se produit pas souvent, c’est-à-dire si l’événement est vraiment exceptionnel et indique une erreur »

La notion de fréquence est mise en avant et doit guider le choix entre une exception et une autre méthode de gestion (vérification préalable ou simple retour de valeur).

En termes de performances, une exception est indéniablement plus coûteuse qu'un simple retour de valeur car elle transporte avec elle la stacktrace utile au débogage. Bien que je ne pense pas qu'elles constituent un goulot d'étranglement pour une application (en tout cas je ne l'ai jamais constaté dans ma jeune carrière), s’assurer qu’elles ne surviennent pas trop souvent garantit que l’impact restera négligeable. D’autant que depuis .NET 9, leur coût a été divisé par deux :

Benchmark .NET 8 vs .NET 9

 

Une autre citation de la documentation : 

« Gérer les conditions courantes pour éviter les exceptions »

Si un cas est susceptible de se produire, alors il y a probablement mieux à faire que de retourner une exception. Je pense par exemple à la validation des données utilisateur. Si vous recevez un formulaire, il est fort probable que certaines données ne soient pas conformes (un champ vide...) : c'est attendu et loin d'être exceptionnel. Cette validation peut se faire via les DataAnnotations ou une bibliothèque comme FluentValidation.

Avec ces recommandations, on est assez loin du dogme qui consiste à interdire leur utilisation. La ligne directrice est assez simple, même si elle ouvre la réflexion sur ce qu'est réellement l’exceptionnel : par exemple, une méthode GetById dans un contexte API est plus susceptible de retourner une erreur que dans un projet MVC rendu côté serveur, qui afficherait explicitement une liste d'élément. On commence à se poser de bonnes questions, n’est-ce pas ?

 

Flux de contrôle et pragmatisme

Le flux de contrôle est un terme un peu pompeux qui correspond simplement à l'ordre dans lequel les instructions de votre programme vont s’exécuter : mon bloc de code déclare une variable, puis appelle une fonction qui va retourner un résultat, une condition s'exécute pour déclencher un événement, que l’on peut éventuellement loguer… bref, c’est le chemin normal de l’exécution.

Utiliser les exceptions pour gérer le flux de contrôle, c’est interrompre le chemin normal d’exécution prévu par le programme pour en prendre un autre… et c'est mal, terriblement mal paraît-il. C'est une mauvaise pratique, un anti-pattern, peut-être même qu'une portée de chatons meurt quand vous vous en servez de cette façon.

try
    {
        if (age >= 18)
            throw new AdultException();
        else
            throw new MinorException();
    }
    catch (AdultException)
    {
        Console.WriteLine("Vous êtes majeur !");
    }
    catch (MinorException)
    {
        Console.WriteLine("Vous êtes mineur !");
    }
}

Un exemple complétement absurde de contrôle de flux par des exceptions, à ne pas reproduire chez vous !

Pourtant, dans des applications du monde réel, les exceptions sont parfois utilisées d'une façon qui pourrait être considérée comme du contrôle de flux. On retrouve mêmes des traces dans des librairies officielles MS comme Azure Blob Storage, qui gère le stockage de fichiers.

private async Task<Response<bool>> ExistsInternal(bool async)
{
    try
    {
        Response<BlobProperties> response =
            await BlobRestClient.Blob.GetPropertiesAsync()
            .ConfigureAwait(false);

        return Response.FromValue(true, response.GetRawResponse());
    }
    catch (RequestFailedException storageRequestFailedException)
    when (storageRequestFailedException.ErrorCode == Constants.Blob.NotFound)
    {
        return Response.FromValue(false, default);
    }
}

BlobBaseClient.cs sur le package Azure Blob Storage

Quand on conseille de « gérer les conditions courantes », il est étonnant de voir certaines exceptions utilisées pour renvoyer un booléen. Ici pour vérifier qu'un fichier existe, la méthode ExistsAsync() utilise un try/catch, et si RequestFailedException est attrapée, c'est que le fichier n'existe pas, et on retourne false. C’est un cas concret d’utilisation d’exception pour contrôler le flux.

Je doute que ce soit le stagiaire de 3ème qui est écrit ce code. A mon sens, c'est un choix pragmatique, probablement dicté par des contraintes techniques du SDK Azure. Peut être que parfois, renvoyer une exception est la solution la plus simple à maintenir et la meilleure chose à faire par rapport au contexte...même si ça s'apparente à une forme de contrôle de flux.

 

Le sauveur result pattern

Une alternative aux exceptions est le Result Pattern issu de la programmation fonctionnelle. C’est simple : il est recommandé partout et chaque influenceur YouTube ou LinkedIn dans le petit monde du .NET possède sa vidéo « Don't throw exceptions in C#. Do this instead ».

Je ne suis pas très fan de son utilisation, le remède est pire que le mal.

C# n'étant pas un langage fonctionnel et en attendant un éventuel support des unions discriminées avec la version 15, il n’existe aucun moyen natif de savoir si un résultat est un succès ou un échec. Il faut donc bricoler sa propre implémentation avec une classe custom ou utiliser une librairie existante comme OneOf ou Ardalis.Result. Mais est-ce que j'ai vraiment envie d'ajouter une dépendance externe pour ça ? Pas vraiment.

public class Result<TValue, TError>
{
    public TValue Value { get; }
    public TError Error { get; }
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;

    private Result(TValue value, TError error, bool isSuccess)
    {
        Value = value;
        Error = error;
        IsSuccess = isSuccess;
    }

    public static Result<TValue, TError> Success(TValue value) =>
        new Result<TValue, TError>(value, default, true);

    public static Result<TValue, TError> Failure(TError error) =>
        new Result<TValue, TError>(default, error, false);

    public void Match(Action<TValue> onSuccess, Action<TError> onFailure)
    {
        if (IsSuccess) onSuccess(Value);
        else onFailure(Error);
    }
}

Une implémentation parmi d'autres du Result Pattern en C#

Ce pattern pollue bien plus le code que ne pourrait le faire une exception. Chaque méthode de la pile applicative devra gérer le retour Result<TValue, TError> et ses possibilités. On travaille avec des architectures en couche et le faire sur plusieurs niveaux devient fastidieux et prend du temps, pour un gain en performance relatif. D'un certain point de vue, c’est un retour en arrière et ce pattern réintroduit une structure héritée des premiers langages de programmation : la logique des codes d'erreurs à vérifier et à propager manuellement. C’est précisément pour éviter cela que les exceptions ont été inventées.

 

Pour conclure

Aujourd'hui, j'ai tendance à utiliser les exceptions de façon décomplexée. Toujours en gardant à l'esprit les recommandations de Microsoft et cette notion de fréquence, mais sans jamais voir leurs utilisations comme un renoncement ou une défaite (ce qui était un peu le cas au début de mon apprentissage du C#). Elles sont au cœur de l'écosystème, et il n'y a aucune honte à utiliser ce que le langage propose. Ne pas le faire me donne l'impression de lutter contre lui et demande tout simplement plus d'effort.

Je travaille principalement sur des projets web, et ASP.NET s’appuie sur un pipeline de middlewares. Cela rend très simple la mise en place d’un gestionnaire d’exceptions global, capable d’intercepter les erreurs et de renvoyer une réponse adaptée. Les dernières versions ont d’ailleurs encore simplifié ce mécanisme avec l’introduction de l’interface IExceptionHandler. Ce sera justement le sujet de mon prochain article 😉