Où catch et la journalisation doivent-ils se situer dans la gestion des exceptions ?

· Mis à jour le: · · Gestion des exceptions, Journalisation, Gestion des erreurs, Conception, C# / .NET

Table des matières

  1. La conclusion d’abord
  2. catch, la journalisation et la gestion des erreurs sont des choses différentes
    • 2.1. Catcher
    • 2.2. Journaliser
    • 2.3. Gérer les erreurs
    • 2.4. Traduire les exceptions
  3. Le tableau de décision à consulter en premier
  4. Que faire, à quel endroit, dans la hiérarchie d’appels
    • 4.1. Le helper / utilitaire / méthode privée le plus profond
    • 4.2. Frontières d’E/S externes : Repository / Gateway / wrapper SDK
    • 4.3. Application Service / UseCase
    • 4.4. Frontières UI / HTTP / Job / Message
    • 4.5. Le gestionnaire d’exceptions non gérées final
    • 4.6. Vue le long d’une seule chaîne d’appels
  5. Séparer les échecs attendus des exceptions inattendues
  6. Où et combien de fois faut-il journaliser ?
  7. Anti-patterns fréquents
  8. Liste de vérification pour la revue
  9. Aide-mémoire rapide
  10. Résumé
  11. Références
  12. Articles connexes

1. La conclusion d’abord

  • Le principe est : ne pas catcher largement dans les couches profondes. Rapprochez le catch des frontières où une unité d’échec peut être définie.
  • Pour la journalisation, la base est un seul log principal par échec. Si chaque couche continue de journaliser la même exception en Error, cela devient pénible pour le lecteur.
  • La responsabilité de la couche la plus profonde est le nettoyage, le rollback local, la traduction de l’exception et, si nécessaire, un retry limité. Si elle relance l’exception, elle n’écrit normalement pas le log principal à cet endroit.
  • Les frontières de traitement — une opération à l’écran, une requête HTTP, un job, un message — ont tendance à être l’endroit le plus naturel pour le log principal.
  • Les échecs attendus doivent être transformés en résultats à l’échelle du cas d’usage. Il n’est pas nécessaire que tout continue à être relancé sous forme d’exception jusqu’en haut.
  • AppDomain.UnhandledException, le DispatcherUnhandledException de WPF, le ThreadException de WinForms, le gestionnaire d’exceptions d’ASP.NET Core et le traitement final des exceptions de l’hôte sont moins des points de récupération que des derniers points d’enregistrement.
  • L’OperationCanceledException causée par une annulation de l’utilisateur ou un arrêt n’est normalement pas traitée comme une Error.
  • En cas de doute, vérifiez dans cet ordre :
    1. Cet endroit peut-il vraiment prendre la décision ?
    2. L’unité d’échec est-elle identifiable ici ?
    3. Peut-on restaurer ou reconstruire l’état ici ?
    4. Si l’on journalise ici, la même exception sera-t-elle aussi journalisée plus haut ?

En résumé, la règle est : catcher non pas là où c’est possible, mais là où l’on peut décider avec responsabilité.

2. catch, la journalisation et la gestion des erreurs sont des choses différentes

2.1. Catcher

catch signifie recevoir une exception une fois et changer le déroulement du traitement. Mais cela, en soi, n’est pas une récupération.

Par exemple, même si une méthode de niveau inférieur reçoit une exception, si elle :

  • ne sait pas ce qui doit être montré à l’utilisateur
  • ne sait pas si cet échec doit arrêter tout l’écran ou seulement faire échouer cette opération
  • ne sait pas si la request ou le job peut continuer

alors cet endroit n’est généralement pas un bon endroit pour catcher.

2.2. Journaliser

Un log n’est pas seulement la trace du fait qu’« une exception s’est produite », mais un enregistrement de quel travail a échoué, afin de pouvoir le retracer ensuite.

C’est pourquoi un bon emplacement de journalisation dispose généralement de certains des éléments suivants.

  • requestId / traceId
  • userId
  • orderId / fileId / batchId
  • quel numéro d’élément dans l’entrée
  • quelle opération à l’écran
  • quelle file d’attente, quel message

Les helpers profonds et les fonctions partagées connaissent souvent les détails techniques mais manquent de ce contexte. Ainsi, l’endroit qui connaît les détails techniques et l’endroit qui connaît le contexte opérationnel ne sont souvent pas le même endroit.

2.3. Gérer les erreurs

Par gestion des erreurs, on entend un traitement comme celui-ci.

  • Afficher un message d’erreur à l’écran
  • Retourner un 4xx / 5xx en HTTP
  • Faire échouer un seul élément et passer au suivant
  • Réinitialiser le sous-système
  • Terminer le processus et laisser le redémarrage au superviseur
  • Libérer les ressources et sortir proprement

Autrement dit, décider de la forme que prend l’échec du point de vue de l’appelant ou de l’utilisateur.

2.4. Traduire les exceptions

En pratique, entre catch et le « traitement », il y a un travail important supplémentaire. C’est la traduction.

Par exemple, si l’on laisse fuir directement vers l’UI ou un Controller :

  • HttpRequestException
  • IOException
  • JsonException
  • des exceptions spécifiques au driver de base de données
  • des exceptions spécifiques au SDK d’un fournisseur

les couches supérieures commencent à connaître les préoccupations internes de l’implémentation inférieure.

C’est pourquoi, à la frontière, on les convertit en échecs qui ont du sens à ce niveau, comme :

  • « Impossible de se connecter au service de paiement »
  • « Le format CSV était corrompu »
  • « Impossible d’écrire vers la destination »
  • « La réponse de l’appareil était invalide »

Le point important ici est que traduire et journaliser ne sont pas la même chose. Si l’on se contente de traduire puis de relancer, on n’écrit normalement pas le log principal.

3. Le tableau de décision à consulter en premier

Le plus simple est de commencer par fixer l’orientation générale avec ce tableau.

Emplacement Politique de base Log principal Responsabilités principales
helper / utilitaire / méthode privée En principe, ne pas catcher largement Non Nettoyage via finally, rollback local, enrichissement minimal du contexte
Repository / Gateway / wrapper SDK Catcher uniquement des exceptions spécifiques Généralement non Traduction d’exception, retry limité, abandon des connexions et handles
Application Service / UseCase Transformer les échecs attendus en résultats Si l’on avale, journaliser ici si nécessaire Définition de l’unité d’échec, gestion des échecs partiels, décisions au niveau du cas d’usage
Frontière UI / Controller / API / Job / Message Le principal récepteur des exceptions inattendues C’est généralement ici que se trouve le log principal Réponses destinées à l’utilisateur, réponses HTTP, poursuite vers l’élément suivant, décision d’abandon
Gestionnaire d’exceptions non gérées / frontière finale de l’hôte La dernière ligne contre les fuites Critical Enregistrement final, flush, dump, chemin de sortie / redémarrage

En schéma, cela ressemble à ceci.

NonOuiNonOuiNonOuiUne exception s'est produiteCet endroit peut-il décider du retry / de la transformation en résultat / de la poursuite ?En principe, ne pas catcher ; laisser propagerEst-ce une frontière de couche ?Nettoyage local uniquementTraduire en exception significative si nécessaireL'unité d'échec et le contexte opérationnel sont-ils connus ici ?Ne pas écrire le log principal ; transmettre vers le hautÉcrire le log principal une fois et décider de la réponseSi nécessaire : sortir / réinitialiser / passer à l'élément suivant

Ce schéma met en évidence deux points.

  1. La première raison de catcher est la récupération ou le nettoyage — pas la journalisation.
  2. La première raison de journaliser est que le contexte opérationnel est disponible — pas le fait d’avoir repéré une exception.

4. Que faire, à quel endroit, dans la hiérarchie d’appels

4.1. Le helper / utilitaire / méthode privée le plus profond

Ici, la règle de base est de ne pas catcher largement.

Des endroits comme la conversion de chaînes, le parsing, le calcul, la mise en forme interne et les helpers partagés ne peuvent pas décider :

  • de quelle opération à l’écran il s’agissait
  • de quelle request il s’agissait
  • si faire échouer seulement cette fois est acceptable
  • si tout l’écran doit se fermer

Ce que cette couche est autorisée à faire se limite principalement à :

  • Libérer les ressources dans finally
  • Annuler (rollback) un état local partiellement modifié
  • Ajouter un contexte minimal au message d’exception
  • Remplacer par un type d’exception plus approprié
  • Détruire les objets qui ne sont plus réutilisables

À l’inverse, voici les styles à éviter.

  • catch (Exception) puis retourner null / false / un tableau vide
  • Afficher une MessageBox ici
  • Journaliser en Error ici puis relancer
  • « Continuer quand même » alors que l’état ne peut pas être restauré

Le motif le plus dangereux est d’échouer après avoir partiellement modifié son propre état, puis de continuer à l’utiliser tel quel. Dans ce cas, il faut soit le restaurer sur place si possible, soit considérer l’objet comme jetable si ce n’est pas possible.

4.2. Frontières d’E/S externes : Repository / Gateway / wrapper SDK

C’est une couche où la raison de catcher est claire.

En effet, c’est ici que les préoccupations d’implémentation de la couche inférieure remontent à la surface.

  • Exceptions du driver de base de données
  • Exceptions de communication HTTP
  • Exceptions d’E/S de fichiers
  • Exceptions spécifiques à COM / P/Invoke / SDK d’un fournisseur
  • Exceptions des bibliothèques de parsing et des sérialiseurs

Ce que fait cette couche se résume à peu près à quatre choses.

  1. Catcher des exceptions spécifiques Pas une Exception large, mais des exceptions spécifiques porteuses de sens.

  2. Traduire en échecs significatifs De sorte que les couches supérieures n’aient pas besoin de connaître directement les détails internes des couches inférieures.

  3. Si vous faites un retry localement, faites-le ici Mais les conditions sont strictes :
    • L’échec est connu comme étant temporaire
    • L’opération est idempotente
    • La limite de tentatives et le délai d’attente sont définis
    • Le comportement final en cas d’échec est clair Seulement lorsque ces quatre conditions sont réunies.
  4. Abandonner les connexions et handles cassés « Reconstruire la connexion » est souvent plus sûr que « continuer avec le même objet ».

La politique de journalisation ici reste stable si l’on raisonne ainsi.

  • Si l’on relance vers le haut, on n’écrit normalement pas le log principal
  • Si l’on avale l’exception ici pour la transformer en résultat, on émet les logs et métriques nécessaires à ce moment-là
  • Traiter chaque tentative de retry dans la plage Debug / Information / Warning, et n’enregistrer fermement que l’échec final

Cette couche est un endroit pour traduire, et non, en général, un endroit pour prendre la décision finale.

4.3. Application Service / UseCase

C’est la couche qui décide « comment ce travail échoue ».

Des choses comme :

  • une opération de sauvegarde
  • la confirmation d’une commande
  • l’import CSV
  • le traitement d’un élément de batch
  • l’application d’un message

— des unités cohérentes en tant que cas d’usage — se trouvent ici.

Cette couche peut prendre des décisions comme celles-ci.

  • Les erreurs de validation ne font échouer que cette tentative
  • NotFound correspond à un 404
  • Les violations de règles métier attendent une correction de l’utilisateur
  • Une ligne CSV incorrecte est journalisée en Warning et le traitement continue
  • Une panne externe temporaire fait échouer l’opération entière
  • Abandonner les résultats intermédiaires et recommencer depuis le début

Autrement dit, c’est l’endroit où l’unité d’échec peut être décidée.

Cette couche convient à des tâches comme :

  • Transformer les échecs attendus en Result ou en DTO d’échec
  • Agréger les échecs partiels
  • Décider combien d’échecs tolérer avant d’arrêter
  • Convertir en codes d’erreur ou en clés de message destinées à l’utilisateur

À l’inverse, ce que cette couche ne devrait pas faire, c’est intégrer trop d’affichage UI ou de construction du corps de réponse HTTP. La séparation est plus nette si cette couche décide jusqu’au sens au niveau du cas d’usage, et laisse la présentation finale à la frontière.

4.4. Frontières UI / HTTP / Job / Message

C’est ici que se trouve généralement l’emplacement du log principal dans la plupart des applications.

Des unités comme :

  • un appui sur le bouton « Enregistrer » dans WinForms / WPF
  • une requête HTTP dans ASP.NET Core
  • un message dans un worker
  • un élément d’entrée dans un batch
  • une exécution d’un job planifié

Cet endroit connaît :

  • quelle était l’opération
  • de qui était l’opération
  • quel numéro d’élément c’était
  • quelle request / batch / message c’était
  • quoi retourner à l’utilisateur ou à l’appelant en cas d’échec

Il prend donc naturellement les rôles suivants :

  • recevoir collectivement les exceptions inattendues ici
  • écrire le log principal une fois, avec le contexte
  • convertir en boîte de dialogue d’erreur, HTTP 500, Problem Details, échec de job, poursuite vers l’élément suivant, etc.

Ce qui compte à ce niveau n’est pas le fait de catcher largement en soi, mais le fait d’avoir défini ce qui est retourné après avoir catché largement.

Pour les batchs et les queues, penser en deux étapes améliore la clarté.

  • Catcher à la frontière par élément Décider si faire échouer seulement cet élément et continuer est acceptable
  • Ne pas tout étouffer largement dans la boucle parente Si la boucle parente meurt, s’appuyer sur le redémarrage de tout le processus

« Faire échouer élément par élément et continuer » et « la boucle parente reste silencieusement en vie après une exception inattendue » sont des choses complètement différentes.

4.5. Le gestionnaire d’exceptions non gérées final

C’est la dernière ligne de défense. Ce n’est pas un point de récupération magique.

Les représentants typiques sont :

  • AppDomain.UnhandledException
  • Application.DispatcherUnhandledException de WPF
  • Application.ThreadException de WinForms
  • le middleware et les gestionnaires de gestion d’exceptions d’ASP.NET Core
  • le traitement final des exceptions dans Generic Host / les workers / BackgroundService

Les responsabilités principales de cette couche se limitent au plus à :

  • Le log final
  • Flush
  • Un chemin vers la collecte de dump
  • Sauvegarder les informations de session et le contexte récent
  • Préparer les codes de sortie et le chemin de redémarrage

À l’inverse, il y a des raisons de ne pas trop en attendre.

  • Le fait d’arriver jusqu’ici indique généralement une lacune de conception en amont
  • L’état est peut-être déjà corrompu
  • Des locks peuvent être détenus, ce qui rend un traitement lourd dangereux ici
  • Même si continuer semble possible en apparence, continuer n’est pas forcément sûr

Il existe aussi des précautions pratiques spécifiques à .NET à connaître.

  • AppDomain.UnhandledException est un événement destiné à la notification et à l’enregistrement des exceptions non gérées. Y intégrer trop de logique de récupération est dangereux.
  • Le DispatcherUnhandledException de WPF offre la possibilité de définir Handled = true pour continuer en apparence, mais juger si la récupération est possible vient d’abord.
  • Le ThreadException de WinForms peut aussi laisser l’application dans un état inconnu après traitement.
  • Le middleware de gestion d’exceptions d’ASP.NET Core doit être placé tôt dans le pipeline pour pouvoir catcher les exceptions de ce qui suit.
  • Une exception non gérée dans un BackgroundService est, à partir de .NET 6, journalisée et, par défaut, arrête l’hôte. Arrêter et s’appuyer sur la stratégie de redémarrage est parfois plus sûr qu’étouffer tout dans la boucle parente.

Dans les applications de bureau en particulier, la voie consistant à « catcher l’exception non gérée et continuer » existe. Mais pouvoir continuer et avoir raison de continuer sont deux choses différentes.

4.6. Vue le long d’une seule chaîne d’appels

Par exemple, considérons un flux comme celui-ci.

Frontière UI / Controller / JobApplication Service / UseCaseDomain / logique métierRepository / Gateway / wrapper SDKDB / HTTP / Fichier / SDK fournisseur

Les rôles se répartissent alors à peu près ainsi.

Bouton Enregistrer → SaveOrderUseCasePaymentGateway → HTTP

  • PaymentGateway
    • Reçoit les échecs de communication et les réponses malformées
    • Les traduit en « échec de connexion au service de paiement » / « réponse invalide du service de paiement »
    • S’il y a retry, le fait ici, conditionnellement
    • S’il relance, n’écrit normalement pas le log principal
  • SaveOrderUseCase
    • Transforme les échecs attendus comme un refus de paiement en résultats
    • Le traite comme « seule cette confirmation de commande a échoué »
    • Met en forme le résultat d’échec pour que l’UI ou l’API puisse le retourner facilement
  • Gestionnaire du bouton UI / Controller
    • Reçoit collectivement les exceptions inattendues
    • Écrit le log principal avec orderId, userId, requestId
    • Convertit en boîte de dialogue ou en réponse 500 / 503
  • Le gestionnaire d’exceptions non gérées
    • N’enregistre que ce qui a fui jusqu’ici
    • Effectue les dumps et le flush final
    • Priorise le chemin de sortie, pas la récupération

Avec cette répartition, on obtient la forme suivante : détails techniques enfermés en bas, contexte opérationnel ajouté en haut, décisions prises à la frontière.

5. Séparer les échecs attendus des exceptions inattendues

Le point le plus important sur ce sujet est de ne pas traiter tout comme la même « exception ».

Commençons par cette répartition.

Type d’échec Premier endroit à traiter Traitement typique
Défauts de validation Frontière UseCase / request Retourner comme une erreur de saisie
NotFound / Conflict UseCase / Controller 404 / 409 ou message à l’écran
Annulation utilisateur / arrêt Frontière de l’opération Traiter comme une annulation. Normalement pas Error
Une ligne CSV incorrecte Frontière par ligne Enregistrer en Warning, continuer
Timeouts temporaires qui échouent finalement Frontière E/S à frontière request Retourner comme échec après les tentatives
NullReferenceException, ruptures d’invariants Frontière request / job Log principal et réponse d’échec
AccessViolationException, OutOfMemoryException sévère, signes de corruption à la frontière native Frontière finale Critical, tendance à la sortie

Les échecs attendus sont des échecs qui peuvent être décidés à l’avance par la conception. Les exceptions inattendues sont des échecs après lesquels il est douteux que l’état puisse encore être fait confiance.

Séparer simplement ces deux catégories réduit des accidents comme :

  • Journaliser NotFound en Error à chaque fois
  • Traiter une annulation utilisateur comme une panne
  • Laisser passer une rupture d’invariant réellement dangereuse comme « échec juste cette fois »

6. Où et combien de fois faut-il journaliser ?

Dans la conception de la journalisation, décider qui écrit le log principal compte davantage que l’emplacement du catch.

Il y a six règles de base.

  1. Un seul log principal Error / Critical par échec
  2. Les couches inférieures font traduction et enrichissement du contexte si nécessaire
  3. La frontière supérieure écrit le log principal avec l’unité d’échec et le contexte opérationnel
  4. Seule la couche qui avale un échec sur place en porte la responsabilité d’enregistrement
  5. Ne pas journaliser les échecs attendus en Error à chaque fois
  6. Garder OperationCanceledException séparé des logs d’échec ordinaires

Voici un tableau approximatif des emplacements de journalisation.

Situation Endroit principal pour journaliser Niveau indicatif Remarque
Erreur de validation Frontière request / use case Information ou pas de log Un échec contractuel, pas une panne
Annulation utilisateur / arrêt Frontière de l’opération Debug / Information Normalement pas Error
Échec temporaire pendant le retry La couche qui porte le retry Debug / Warning Ne pas faire de bruit avant l’échec final
Tentatives épuisées, échec Frontière request / job, ou la couche qui l’avale Warning / Error Enregistrer avec l’unité d’échec
Exception inattendue faisant échouer toute la request Frontière request / UI / job Error Joindre requestId, userId, entityId
Classe fin de processus Frontière d’exception non gérée Critical Flush, dump, chemin de redémarrage

Un motif très courant en pratique est ce genre de journalisation dupliquée.

  • Le Repository journalise en Error
  • Le Service journalise la même exception en Error
  • Le Controller journalise encore en Error
  • Le gestionnaire d’exceptions non gérées final journalise aussi en Critical

Ainsi, un seul incident produit plusieurs copies de la même pile d’appels côte à côte. Ce que le lecteur veut réellement, ce n’est pas quatre copies de la même pile d’appels, mais un log principal, plus, si nécessaire, un petit nombre de logs de support.

Autrement dit, la règle de base est : journaliser une fois, avec autant de contexte que nécessaire.

7. Anti-patterns fréquents

7.1. catch (Exception) en profondeur, en retournant null / false

Cela fait facilement perdre l’information sur la cause. Pire, l’appelant ne peut plus distinguer « les données n’existaient vraiment pas » de « quelque chose s’est cassé en chemin ».

7.2. Journaliser en Error à chaque couche avant de relancer

La source la plus courante de logs dupliqués.

  • Les couches inférieures se contentent de traduire
  • La frontière supérieure écrit le log principal

Avec cette répartition, cela diminue considérablement.

Pour relancer en C#, la règle est d’utiliser throw; afin de préserver la pile d’appels.

7.3. Les couches de bibliothèque ou les composants partagés affichent directement l’UI

Quand un composant partagé affiche une MessageBox ou décide directement d’un corps de réponse HTTP, la réutilisabilité et la séparation des responsabilités s’effondrent toutes les deux. Les couches inférieures sont plus sûres lorsqu’elles se limitent à retourner un échec significatif.

7.4. Journaliser OperationCanceledException comme une panne en Error

L’annulation fait partie du flux de contrôle. La journaliser en Error à chaque fois enterre les vraies pannes.

7.5. Faire un retry sans précaution malgré des effets de bord externes

De nombreuses opérations sont des accidents en puissance si elles sont exécutées deux fois : envoi d’e-mail, facturation, commandes d’appareil, déplacement de fichiers. Ne faites un retry que lorsque le caractère temporaire et l’idempotence sont tous deux visibles.

7.6. Essayer de tout récupérer dans le gestionnaire d’exceptions non gérées final

C’est la dernière police d’assurance. Ce n’est pas l’endroit à placer au centre de votre conception.

La stratégie de récupération est plus sûre une couche plus tôt — à la frontière request / job / sous-système.

8. Liste de vérification pour la revue

Lors de la revue de la gestion des exceptions, procéder dans cet ordre laisse peu de trous.

  • Peut-on énoncer en une phrase quelle décision ce catch existe pour prendre ?
  • Cet endroit peut-il vraiment décider du retry / de la transformation en résultat / de la poursuite / de la réponse utilisateur ?
  • Si l’on journalise ici, le même échec sera-t-il aussi journalisé en Error plus haut ?
  • Les exceptions spécifiques à l’implémentation inférieure sont-elles traduites en échecs significatifs à la frontière ?
  • L’état partiellement cassé peut-il être restauré ici ? Sinon, est-il traité comme jetable ?
  • OperationCanceledException est-elle séparée des échecs ordinaires ?
  • Est-il clair s’il s’agit d’une poursuite par élément, d’un échec par request, ou d’une sortie de processus ?
  • Le gestionnaire d’exceptions non gérées final est-il censé enregistrer, et non récupérer ?
  • Les logs portent-ils le contexte de l’unité d’échec — requestId / userId / batchId / fileId / rowNumber ?
  • Traite-t-on de la même façon les « échecs attendus » et les « ruptures d’invariants » ?

Ce qui rapporte le plus dans cette liste, c’est de mettre en mots « que décide ce catch ? » à chaque fois. Un catch pour lequel on ne peut pas répondre à cela est généralement inutile, ou placé trop profondément.

9. Aide-mémoire rapide

Enfin, condensé au maximum, cela donne ce tableau.

Situation catch Log Gestion des erreurs
helper / utilitaire En principe, non Non Non
Repository / Gateway / wrapper SDK Exceptions spécifiques uniquement Généralement pas de log principal Traduction, retry local, abandon des connexions
UseCase / Application Service Reçoit les échecs attendus Si nécessaire en cas d’avalement Transformation en résultat, gestion des échecs partiels
Frontière UI / Controller / request / item / job Reçoit largement les exceptions inattendues Log principal Réponse, message, poursuite / abandon
Gestionnaire d’exceptions non gérées Seulement ce qui a fui Critical Enregistrement final, chemin de sortie

En cas de doute, ces cinq points seuls suffisent.

  1. Ne pas saisir largement dans les couches profondes
  2. Catcher aux frontières
  3. Un seul log principal
  4. La couche qui avale porte la responsabilité
  5. L’exception non gérée finale signifie enregistrement et chemin de sortie

10. Résumé

La gestion des exceptions n’est pas une question de « on peut catcher n’importe où, donc on catche partout ».

L’ordre à vérifier est à peu près celui-ci, et cela suffit.

  1. Cet endroit peut-il vraiment prendre la décision ?
  2. L’unité d’échec est-elle identifiable ici ?
  3. Peut-on restaurer ou reconstruire l’état ici ?
  4. Journaliser ici provoquerait-il une duplication ?
  5. Est-ce un point de récupération, ou le dernier point d’enregistrement ?

Vérifier dans cet ordre rend l’organisation de la hiérarchie d’appels beaucoup plus facile.

Les trois points qui comptent le plus :

  • Couches profondes : principalement traduction et nettoyage
  • Frontières : principalement décisions et log principal
  • Le gestionnaire d’exceptions non gérées final : principalement enregistrement et chemin de sortie

Autrement dit, la règle de base est : catcher les exceptions aux frontières, y attacher le contexte, et ne les traiter que là où la récupération est possible.

Une fois cela établi, les revues de code comme les enquêtes sur incidents deviennent beaucoup moins hésitantes.

11. Références

12. Articles connexes

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

À quel niveau faut-il catcher les exceptions dans une application en couches ?
Le principe est de ne pas catcher largement dans les couches profondes, mais de rapprocher le catch des frontières où une unité d'échec peut être définie. Une frontière de traitement comme une opération à l'écran, une requête HTTP, un job ou un message constitue un point de réception naturel. La règle de base est de catcher non pas là où c'est possible, mais là où l'on peut décider avec responsabilité du retry, de la transformation en résultat ou de la possibilité de continuer. Dans les helpers et utilitaires profonds, on se limite au nettoyage via finally, au rollback local et à la traduction de l'exception.
Faut-il journaliser une exception à chaque couche ?
Non : la règle de base est un seul log principal Error / Critical par échec. Si le Repository journalise en Error, que le Service journalise la même exception en Error, puis que le Controller la journalise encore en Error, un seul incident produit plusieurs copies de la même pile d'appels, ce qui gêne la lecture. Les couches inférieures se limitent à traduire et enrichir le contexte, tandis que la frontière supérieure, qui dispose du contexte opérationnel comme requestId ou userId, écrit le log principal. Seule la couche qui avale l'exception et la transforme en résultat porte la responsabilité d'enregistrer cet échec.
Comment distinguer les échecs attendus des exceptions inattendues ?
Un échec attendu est un échec que la conception peut décider à l'avance : les défauts de validation ou un NotFound se transforment en résultat au niveau du cas d'usage, sans être journalisés en Error à chaque fois. L'OperationCanceledException due à une annulation par l'utilisateur n'est en général pas non plus traitée comme une Error. À l'inverse, une rupture d'invariant comme une NullReferenceException doit être journalisée en log principal à la frontière request / job et se traduire par une réponse d'échec, tandis qu'une AccessViolationException ou un OutOfMemoryException sévère doit être traitée en Critical, avec une tendance à l'arrêt. Cette seule distinction réduit le risque qu'un échec réellement dangereux passe inaperçu.
Que doit faire le gestionnaire d'exceptions non gérées final ?
AppDomain.UnhandledException, DispatcherUnhandledException de WPF ou ThreadException de WinForms ne sont pas des points de récupération mais le dernier point d'enregistrement. Leurs responsabilités principales se limitent au log final, au flush, à la mise en place de la collecte de dump, ainsi qu'à la préparation du code de sortie et du chemin de redémarrage. Au moment où une exception arrive jusque-là, l'état de l'application est peut-être déjà corrompu ; même si continuer semble possible en apparence, cela ne signifie pas que c'est prudent. La stratégie de récupération est plus sûre lorsqu'elle est placée à la frontière précédente, celle de la request ou du job.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog