Où catch et la journalisation doivent-ils se situer dans la gestion des exceptions ?
· Mis à jour le: · Go Komura · Gestion des exceptions, Journalisation, Gestion des erreurs, Conception, C# / .NET
Table des matières
- La conclusion d’abord
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
- Le tableau de décision à consulter en premier
- 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
- Séparer les échecs attendus des exceptions inattendues
- Où et combien de fois faut-il journaliser ?
- Anti-patterns fréquents
- Liste de vérification pour la revue
- Aide-mémoire rapide
- Résumé
- Références
- Articles connexes
1. La conclusion d’abord
- Le principe est : ne pas catcher largement dans les couches profondes. Rapprochez le
catchdes 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, leDispatcherUnhandledExceptionde WPF, leThreadExceptionde 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’
OperationCanceledExceptioncausé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 :
- Cet endroit peut-il vraiment prendre la décision ?
- L’unité d’échec est-elle identifiable ici ?
- Peut-on restaurer ou reconstruire l’état ici ?
- 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 :
HttpRequestExceptionIOExceptionJsonException- 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.
flowchart TD
A["Une exception s'est produite"] --> B{"Cet endroit peut-il décider du retry / de la transformation en résultat / de la poursuite ?"}
B -- "Non" --> C["En principe, ne pas catcher ; laisser propager"]
B -- "Oui" --> D{"Est-ce une frontière de couche ?"}
D -- "Non" --> E["Nettoyage local uniquement"]
D -- "Oui" --> F["Traduire en exception significative si nécessaire"]
E --> G{"L'unité d'échec et le contexte opérationnel sont-ils connus ici ?"}
F --> G
G -- "Non" --> H["Ne pas écrire le log principal ; transmettre vers le haut"]
G -- "Oui" --> I["Écrire le log principal une fois et décider de la réponse"]
I --> J["Si nécessaire : sortir / réinitialiser / passer à l'élément suivant"]
Ce schéma met en évidence deux points.
- La première raison de catcher est la récupération ou le nettoyage — pas la journalisation.
- 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 retournernull/false/ un tableau vide- Afficher une
MessageBoxici - Journaliser en
Errorici 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.
-
Catcher des exceptions spécifiques Pas une
Exceptionlarge, mais des exceptions spécifiques porteuses de sens. -
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.
- 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.
- 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
NotFoundcorrespond à un 404- Les violations de règles métier attendent une correction de l’utilisateur
- Une ligne CSV incorrecte est journalisée en
Warninget 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
Resultou 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.UnhandledExceptionApplication.DispatcherUnhandledExceptionde WPFApplication.ThreadExceptionde 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.UnhandledExceptionest 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
DispatcherUnhandledExceptionde WPF offre la possibilité de définirHandled = truepour continuer en apparence, mais juger si la récupération est possible vient d’abord. - Le
ThreadExceptionde 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
BackgroundServiceest, à 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.
flowchart LR
A["Frontière UI / Controller / Job"] --> B["Application Service / UseCase"]
B --> C["Domain / logique métier"]
C --> D["Repository / Gateway / wrapper SDK"]
D --> E["DB / HTTP / Fichier / SDK fournisseur"]
Les rôles se répartissent alors à peu près ainsi.
Bouton Enregistrer → SaveOrderUseCase → PaymentGateway → 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
NotFoundenErrorà 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.
- Un seul log principal
Error/Criticalpar échec - Les couches inférieures font traduction et enrichissement du contexte si nécessaire
- La frontière supérieure écrit le log principal avec l’unité d’échec et le contexte opérationnel
- Seule la couche qui avale un échec sur place en porte la responsabilité d’enregistrement
- Ne pas journaliser les échecs attendus en
Errorà chaque fois - Garder
OperationCanceledExceptionsé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
catchexiste 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
Errorplus 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 ?
OperationCanceledExceptionest-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.
- Ne pas saisir largement dans les couches profondes
- Catcher aux frontières
- Un seul log principal
- La couche qui avale porte la responsabilité
- 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.
- Cet endroit peut-il vraiment prendre la décision ?
- L’unité d’échec est-elle identifiable ici ?
- Peut-on restaurer ou reconstruire l’état ici ?
- Journaliser ici provoquerait-il une duplication ?
- 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
- .NET : Bonnes pratiques pour les exceptions
- .NET : Événement System.AppDomain.UnhandledException
- WPF : Événement Application.DispatcherUnhandledException
- Windows Forms : Événement Application.ThreadException
- Gérer les erreurs dans ASP.NET Core
- Middleware ASP.NET Core
- Services Windows utilisant BackgroundService
12. Articles connexes
- Liste de vérification en cas d’exception inattendue - Faut-il quitter l’application ou continuer ? Le tableau de décision à consulter en premier
- Quand on ne peut pas éviter un logger maison, quelles sont les exigences minimales réellement nécessaires ? Exigences pratiques et perspectives de tests d’intégration
- Qu’est-ce que le Generic Host de .NET ? - DI, configuration, journalisation et BackgroundService, organisés d’abord
- Jusqu’où les tests unitaires doivent-ils aller, et où les tests d’intégration doivent-ils prendre le relais ? - Tracer la frontière et un tableau de décision pratique
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Un tableau de décision pour choisir entre arrêt et poursuite après une exception inattendue
Lorsqu'une exception inattendue survient, faut-il arrêter l'application ou la laisser continuer ? Cet article organise la décision sous l...
Concevoir la conservation des journaux et des dumps lors du crash d'une application Windows
Comment combiner journalisation habituelle, marqueur de crash final, WER LocalDumps et processus de surveillance pour conserver, même qua...
Liste de contrôle minimale de sécurité pour le développement d'applications Windows
Pour les applications métier WPF / WinForms / WinUI / C++ / C#, cet article organise sous forme de liste de contrôle les bases concernant...
Gestion des erreurs et stratégie de nouvelle tentative dans Power Automate — éviter qu'un flux qui fonctionnait ne s'arrête sans que personne ne le remarque
Un ensemble de modèles de conception pour éviter qu'un flux Power Automate ne s'arrête sans que personne ne s'en aperçoive. Nous détaillo...
Gestion des erreurs et conception des nouvelles tentatives sous PowerShell — du piège du try/catch aux bonnes pratiques d'exit code et de retry
Cet article présente, du point de vue pratique, la différence entre erreurs terminales et non terminales sous PowerShell, le piège du try...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Applications métier, intégration d'équipements et outils de communication, des besoins au développement.
Conseil technique et revue de conception
Clarification de la stratégie de modification, de la conception et du traitement des actifs existants.
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.
Liens publics