Un tableau de décision pour choisir entre arrêt et poursuite après une exception inattendue

· Mis à jour le: · · Développement Windows, Gestion des exceptions, Conception, C# / .NET, Fiabilité

Télécharger la liste de contrôle Excel (feuilles en japonais et en anglais)

Quand il est question d’une exception inattendue, on a tendance à la ramener à un choix binaire : planter, ou catcher et continuer. Mais en pratique, poser les choses ainsi est un peu brutal.

Ce que l’on veut vraiment savoir, c’est si l’on peut circonscrire l’étendue de ce qui a pu être corrompu.

  • Peut-on faire échouer uniquement cette opération et s’arrêter là ?
  • Suffit-il de réinitialiser uniquement cet écran / cette connexion / ce worker ?
  • Ou l’intégrité de l’ensemble du processus est-elle désormais en question ?

Regarder les choses dans cet ordre permet de bien mieux s’y retrouver.

Dans cet article, en prenant pour cadre les applications Windows en C# / .NET, les applications résidentes, les services Windows et les outils d’intégration d’équipements, nous dressons un tableau de décision réunissant les conditions dans lesquelles il est acceptable de continuer après une exception inattendue, et celles dans lesquelles il vaut mieux arrêter l’application.

1. La conclusion d’abord

  • Avaler l’exception avec catch (Exception) puis continuer est généralement dangereux.
  • Il est acceptable de continuer seulement lorsque trois conditions sont réunies : on peut abandonner l’unité qui a échoué, on peut restaurer l’état partagé, et on peut rendre compte des effets de bord externes.
  • Lorsque la frontière du traitement est claire — une opération UI, une saisie unique, un job unique — il est parfois possible de continuer.
  • À l’inverse, si un état partagé modifiable, une boucle résidente, le thread principal, le code de démarrage, une frontière native ou des signes de corruption mémoire sont en cause, mieux vaut pencher vers l’arrêt.
  • Les exceptions qui remettent en cause « la santé de l’ensemble du processus », comme StackOverflowException, AccessViolationException ou OutOfMemoryException, sont plus sûres à ne pas considérer comme un point de départ pour continuer.
  • WPF et Windows Forms offrent des moyens de récupérer les exceptions non gérées et de donner l’impression de continuer, mais pouvoir continuer et continuer en toute sécurité sont deux choses différentes.
  • Pour les services de longue durée et les applications de surveillance, planter puis être redémarré est souvent plus facile à diagnostiquer et plus sûr que de survivre à moitié cassé.

En somme, l’axe de décision est peut-on restaurer les invariants ?

2. Ce que cet article entend par « exception inattendue »

2.1 Distinguer l’attendu de l’inattendu

Pour commencer, une exception rare et une exception inattendue ne sont pas la même chose.

Par exemple, les cas suivants peuvent être considérés comme attendus, même s’ils sont peu fréquents :

  • L’utilisateur a sélectionné un fichier qui n’existe pas
  • Le correspondant réseau a subi un timeout temporaire
  • Une ligne du CSV importé était corrompue
  • Une opération d’annulation a déclenché OperationCanceledException
  • Une violation d’une règle métier doit faire échouer uniquement ce traitement

Ce sont des cas où la manière de traiter l’échec peut être décidée à l’avance, dès la conception.

À l’inverse, les exceptions inattendues dont traite principalement cet article ressemblent à ceci :

  • Une hypothèse de votre propre code s’est effondrée, déclenchant une NullReferenceException ou une InvalidOperationException
  • Une exception a été levée en pleine mise à jour d’un état partagé, et on ne sait plus jusqu’où les changements ont été appliqués
  • La boucle parente d’une boucle de surveillance ou de traitement de messages s’est arrêtée brutalement
  • Une anomalie est survenue à une frontière COM / P/Invoke / SDK tiers
  • Le diagnostic de santé du processus lui-même est au rouge, comme avec AccessViolationException ou StackOverflowException

Autrement dit, ce sont des cas où l’on ne sait plus « si l’on peut encore faire confiance à l’état de l’application après cette exception ».

2.2 Cela ressemble à un choix binaire, mais il y en a en réalité trois

Ce qui complique ce débat, c’est de considérer « continuer » comme une option unique.

En pratique, cela se décompose généralement en trois niveaux.

Choix Signification
Faire échouer uniquement cette opération et continuer L’écran reste, mais seul cet enregistrement ou cet import est traité comme un échec
Arrêter uniquement le sous-système et continuer Réinitialiser uniquement la connexion, l’écran, le worker ou le processus enfant
Arrêter le processus L’étendue de la corruption d’état est impossible à évaluer, donc on part du principe d’un redémarrage

Dire « l’application continue » recouvre deux réalités bien différentes : continuer comme si de rien n’était, et continuer après avoir isolé la partie endommagée.

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

3.1 Vue d’ensemble

En partant de ce tableau, l’orientation générale se dessine généralement d’elle-même.

Situation Premier choix Raison
Une seule saisie, une seule opération d’écran ou un seul job a échoué, et son état peut être abandonné Plutôt continuer L’unité en échec peut être circonscrite
Après l’exception, l’objet ou la connexion concernée peut être détruite puis recréée Plutôt réinitialiser le sous-système La zone endommagée peut être localisée
L’état partagé a été mis à jour partiellement, sans savoir jusqu’où Plutôt arrêter Les invariants ont peut-être été rompus
Les effets de bord externes (BD / fichiers / commandes d’équipement) sont incomplets, sans pouvoir exclure une duplication ou un défaut d’application Plutôt arrêter La cohérence avec le monde extérieur ne peut plus être évaluée
La boucle de surveillance, de reconnexion ou la boucle parente de traitement des messages s’est arrêtée à cause d’une exception inattendue Plutôt arrêter Perdre silencieusement une partie des fonctionnalités tend à créer un processus zombie
Échec du traitement de démarrage, du chargement de la configuration, de la composition DI ou de l’initialisation d’une dépendance requise Plutôt arrêter en tant qu’échec de démarrage Démarrer à moitié initialisé est plus dangereux
AccessViolationException, StackOverflowException, un OutOfMemoryException sévère, ou des signes de corruption côté natif Plutôt arrêt immédiat La santé de l’ensemble du processus est mise en doute
Le traitement dangereux est isolé dans un processus séparé, et le processus parent reste intact Le parent continue, l’enfant redémarre Le domaine de la panne est déjà isolé
OuiNonNonOuiNonOuiNonOuiException inattendueSignes de corruption mémoire / d'épuisement de la pile / d'épuisement fatal des ressources ?Arrêt / FailFast / redémarrageL'unité en échec peut-elle être abandonnée ?Plutôt arrêterL'état partagé peut-il être restauré / réinitialisé ?Arrêt du sous-système ou du processusPeut-on rendre compte des effets de bord externes ?Continuer, en ne faisant échouer que cette opération

3.2 Ce qu’il faut vérifier avant de regarder le type d’exception

Mieux vaut ne pas décider uniquement à partir du type d’exception. Voici ce qu’il convient de vérifier en premier.

Point à examiner Ce qu’il faut confirmer
Où cela s’est-il produit ? Un événement UI, un job unique, une boucle parente, le code de démarrage, ou une frontière native
Jusqu’où le traitement est-il allé ? L’état mémoire, la BD, les fichiers ou l’état de l’équipement ont-ils changé en cours de route
Étendue possible des dégâts Seulement cet objet, tout l’écran, ou tout le processus
Un rollback est-il possible ? Peut-on détruire puis recréer, ou revenir en arrière via une transaction
Effets de bord externes Envoyé ou non envoyé, la double exécution est-elle sûre, une compensation est-elle possible
Surveillance / redémarrage Existe-t-il un redémarrage automatique ou un chemin de récupération après l’arrêt

3.3 Exceptions à haut risque

Il n’est pas nécessaire de passer en revue tous les types d’exceptions en détail, mais certaines ne doivent jamais être envisagées comme un point de départ pour continuer.

Exception / symptôme Premier choix Pourquoi
StackOverflowException Plutôt arrêt immédiat La pile d’appels s’est effondrée ; on ne peut pas compter sur une récupération normale
AccessViolationException Plutôt arrêt immédiat Accès illégal à une zone mémoire protégée ; une frontière native ou une corruption mémoire est suspectée
OutOfMemoryException Plutôt arrêt Le traitement de récupération, qui suppose lui-même de nouvelles allocations, tend à devenir instable
NullReferenceException / InvalidOperationException inattendue Selon le contexte, mais plutôt vers l’arrêt Une hypothèse propre au code s’est effondrée, et des modifications partielles peuvent subsister
Exception inattendue échappée d’une boucle parente Plutôt arrêter Le cœur de la fonctionnalité est mort alors que le processus, lui, risque de rester en vie
Anomalie provenant d’un callback COM / P/Invoke / SDK tiers De l’arrêt immédiat à une forte tendance vers l’arrêt La sécurité est difficile à juger depuis le seul côté managé

4. Décider en fonction du lieu de l’incident

4.1 Événements UI

Les événements UI comme un clic de bouton, une navigation d’écran, une recherche ou une sélection de fichier laissent une marge de manœuvre relativement large pour continuer. Il y a toutefois des conditions.

Continuer est plus facile dans les cas suivants :

  • L’échec est survenu avant le chargement, et l’état métier n’a pas encore été touché
  • Seul un état temporaire à l’intérieur d’une boîte de dialogue est corrompu, et fermer l’écran suffit à l’éliminer
  • Le ViewModel ou la connexion peuvent être recréés après l’exception
  • On peut dire honnêtement à l’utilisateur que « cette opération a échoué »

À l’inverse, dans les cas suivants, mieux vaut pencher vers l’arrêt :

  • L’écran et l’état du domaine ont tous deux été mis à jour partiellement
  • Un état partagé visible par d’autres écrans — static, singleton, cache — a été touché
  • Après l’exception, seuls l’état d’activation des boutons ou la sélection subsistent, sans que la cohérence globale soit claire
  • Une exception inattendue s’est produite sur le thread UI, et on ne sait plus jusqu’où le rendu ou les notifications ont progressé

4.2 Jobs / requêtes traités un par un

C’est une frontière où il est facile de continuer.

  • Un message
  • Un fichier
  • Une requête HTTP
  • Un job d’import
  • Un élément de traitement par lots

Lorsque de telles unités sont clairement définies, on peut faire échouer uniquement cet élément et passer au suivant.

Il y a toutefois des prérequis :

  • L’unité de l’échec est clairement identifiable de l’extérieur
  • Les modifications partielles sont remises en ordre par une transaction ou une compensation
  • Rejouer le même traitement une seconde fois ne corrompt pas le résultat
  • L’échec peut être redirigé vers une file d’attente de quarantaine ou un journal d’erreurs

4.3 Boucles résidentes / surveillance / traitement de files d’attente

C’est l’endroit où continuer sans précaution est le plus problématique.

Par exemple :

  • Boucles de reconnexion
  • Boucles de surveillance
  • Boucles de consommation de file d’attente
  • Polling périodique
  • Surveillance de l’état des équipements
  • Traitement résident d’une application dans la zone de notification

Ce qui est redoutable avec ce type de traitement, c’est que la boucle parente meurt à cause d’une seule exception inattendue, tandis que le processus, lui, survit.

Ici, il vaut mieux séparer la politique appliquée :

  • Capturer les exceptions attendues à la frontière du traitement de chaque élément
  • Si une exception inattendue s’échappe de la boucle parente, pencher vers l’arrêt du processus

4.4 Traitement de démarrage

Traiter un échec au démarrage avec l’attitude « on démarre quand même et on réfléchira après » finit presque toujours mal.

  • La configuration requise ne peut pas être lue
  • La migration de version a échoué
  • Un dossier ou un certificat requis est manquant
  • L’initialisation d’un service central a échoué
  • La configuration des dépendances est corrompue

Dans ces cas-là, arrêter l’application en la traitant comme un échec de démarrage est la solution la plus claire.

4.5 Frontières natives / COM / P/Invoke / unsafe

Ce domaine mérite une catégorie à part, avec un regard un peu plus strict.

  • COM
  • P/Invoke
  • Le code au-delà de C++/CLI
  • SDK tiers
  • Code natif qui revient via un callback
  • Traitement impliquant unsafe

Il faut particulièrement pencher vers l’arrêt lorsqu’on observe l’un de ces signes :

  • AccessViolationException
  • Symptômes évoquant une corruption du tas ou un double free
  • Anomalies de handle, signes d’accès après libération (use-after-free)
  • Mort subite à la frontière d’un callback

5. Conditions dans lesquelles il est acceptable de continuer

Voici, résumées, les conditions dans lesquelles il est acceptable de continuer. Le principe est que la plupart d’entre elles soient réunies.

Condition Signification
L’unité de l’échec est claire On sait ce que l’on peut abandonner : une opération, un écran, un job, une connexion
L’état peut être abandonné Il peut être détruit puis recréé, ou traité comme jamais appliqué
L’état partagé est protégé La contamination ne se propage pas aux autres fonctionnalités
On peut rendre compte des effets de bord externes On sait si l’envoi a eu lieu, s’il n’a pas eu lieu, ou s’il est possible de renvoyer
On peut être honnête avec l’utilisateur On peut afficher que « ce traitement a échoué »
C’est observable Les journaux, métriques et dumps permettent une investigation ultérieure

6. Conditions dans lesquelles mieux vaut arrêter

À l’inverse, si l’un des points suivants s’applique, mieux vaut pencher vers l’arrêt.

  • On ne sait pas ce qui a été modifié en cours de route
  • Un état partagé modifiable a été touché, et sa cohérence ne peut plus être évaluée
  • La gestion du cycle de vie des verrous, des files d’attente, des threads ou des boucles de surveillance est corrompue
  • On ne peut pas rendre compte d’une duplication, d’une perte ou d’un état incomplet des effets de bord externes
  • L’initialisation du traitement de démarrage ou de l’infrastructure centrale a échoué
  • Une frontière native ou une corruption mémoire est suspectée

À ce niveau, concevoir une récupération facile après le plantage est plus efficace que s’efforcer de continuer proprement.

7. Recommandations par motif typique

Motif Recommandation Raison
Un chemin inexistant a été indiqué via le bouton d’ouverture de fichier Continuer en faisant échouer uniquement cette opération La corruption d’état est locale
Une seule ligne de l’import CSV était corrompue Continuer en faisant échouer cette ligne ou ce fichier L’unité de l’échec est facile à circonscrire
Une NullReferenceException inattendue est survenue en cours d’enregistrement de l’écran De la recréation de l’écran à un arrêt penché On ne sait plus jusqu’où le ViewModel / l’état métier ont changé
Un message de la file d’attente violait une règle métier Continuer en faisant échouer uniquement ce message Il peut être redirigé vers une file de quarantaine
La boucle parente de consommation de la file d’attente s’est arrêtée à cause d’une exception inattendue Plutôt arrêter le processus Le cycle de vie de l’ensemble du worker est corrompu
La configuration requise ne peut pas être lue au démarrage Arrêter en tant qu’échec de démarrage Un démarrage à moitié initialisé est plus dangereux
Une AccessViolationException autour d’un callback de SDK tiers Plutôt arrêt immédiat La possibilité d’une corruption mémoire ne peut pas être ignorée
Seul l’envoi de télémétrie non essentiel a échoué Désactiver uniquement cette fonctionnalité et continuer Le domaine de la panne peut être séparé de la fonctionnalité principale

8. Anti-patterns courants

8.1 Se contenter de journaliser avec catch (Exception) puis continuer

C’est assez dangereux. Cela masque la cause tout en prolongeant facilement la vie d’un état corrompu.

8.2 Essayer de récupérer dans le gestionnaire d’exceptions non gérées de dernier recours

AppDomain.UnhandledException, Application.ThreadException, DispatcherUnhandledException et autres sont utiles comme dernier endroit où consigner l’information, mais ce ne sont pas des points de récupération magiques.

8.3 Retenter (retry) sans précaution alors que des effets de bord externes sont impliqués

Si l’on retente une commande d’équipement, l’envoi d’un e-mail, une facturation, un déplacement de fichier ou une mise à jour de BD sans garantie de sécurité de la ré-exécution, c’est alors l’incident de double exécution qui prend le devant de la scène.

8.4 Laisser l’UI en vie alors que la boucle de surveillance est morte

Une application qui a l’air vivante mais qui ne fait plus son travail est une véritable nuisance.

8.5 Dire « on ne veut pas planter » sans avoir conçu l’application pour cela

Si l’on ne veut pas planter, il y a des éléments à mettre en place au préalable.

  • Redémarrage automatique
  • Restauration de session
  • Sauvegarde des résultats intermédiaires
  • Sécurité de la ré-exécution
  • Isolation du domaine de panne

9. Points à clarifier au moment de l’implémentation

9.1 Rapprocher les points de catch des frontières

Plutôt que de tout catcher en profondeur, il est plus facile de garder les choses claires en interceptant les exceptions aux endroits où une unité d’échec peut être définie, comme :

  • Frontière d’opération UI
  • Frontière d’une requête
  • Frontière d’un job
  • Frontière d’une connexion
  • Frontière du processus

9.2 Séparer les exceptions attendues des exceptions inattendues

  • Attendu : validation, not found, timeout, annulation, violation de règle métier
  • Inattendu : hypothèse rompue, fuite hors d’une boucle parente, anomalie de frontière native, signes de corruption mémoire

9.3 Réduire la taille de l’état partagé

Plus l’état partagé modifiable est vaste, plus la décision de continuer devient difficile. À l’inverse, plus on parvient à le confiner à l’intérieur d’un seul écran, d’une seule session, d’un seul worker, plus il est facile de confiner également les échecs.

9.4 Déporter les traitements dangereux vers un processus séparé

Pour tout ce dont on ne veut pas que les dégâts se propagent en cas de plantage — COM / ActiveX / SDK tiers / code unsafe / traitement d’images lourd / contrôle d’équipements externes — l’isoler dans un processus séparé est très efficace.

9.5 Les gestionnaires d’exceptions non gérées servent à « consigner », pas à « récupérer »

  • Les informations sur l’exception
  • Le contexte de l’opération
  • Les journaux importants juste avant l’incident
  • La configuration / la version / la destination de connexion
  • Un chemin de collecte des dumps

Réunir ces éléments et privilégier une configuration permettant de creuser après le plantage conduit, au final, à plus de stabilité.

9.6 Ne pas surestimer les événements d’exceptions non gérées de WPF / WinForms

Dans WPF, mettre Handled = true dans DispatcherUnhandledException permet effectivement de continuer après une exception non gérée. Dans Windows Forms également, sur le thread UI principal, Application.ThreadException et le paramètre SetUnhandledExceptionMode permettent de choisir la manière dont l’application s’arrête.

Mais pouvoir continuer ainsi et réunir les conditions de récupération sont deux questions distinctes.

10. Résumé

Lorsqu’une exception inattendue survient, la question à se poser n’est pas « peut-on catcher cette exception ? » mais peut-on encore faire confiance à l’état de l’application après coup ?

Comme séquence de décision, ceci suffit généralement :

  1. L’unité en échec peut-elle être abandonnée ?
  2. L’état partagé peut-il être restauré ou recréé ?
  3. Peut-on rendre compte des effets de bord externes ?
  4. La santé de la mémoire, des threads et des frontières natives peut-elle être considérée comme fiable ?

Si l’on est confiant sur ces quatre points, on peut continuer. Sinon, mieux vaut pencher vers l’arrêt.

En particulier pour les applications de longue durée, les applications de surveillance, les services et l’intégration d’équipements, il existe de nombreuses situations où survivre tout en étant cassé est plus dangereux que planter honnêtement.

La gestion des exceptions n’est pas l’art de ne jamais planter. C’est une conception qui réduit l’ampleur des dégâts, qui arrête honnêtement l’application quand elle est cassée, et qui facilite la reprise.

11. Références

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.

Conseil technique et revue de conception

Ce sujet porte sur la clarification de la politique de gestion des exceptions, des frontières de panne, de la stratégie de redémarrage et des critères de décision pour la poursuite ou non du traitement ; il se prête donc bien à une consultation technique ou à une revue de conception.

Analyse des bugs et des causes

La démarche consistant à déterminer, après une exception inattendue, s'il faut continuer ou s'arrêter — en tenant compte de la corruption d'état et des effets de bord externes — s'inscrit naturellement dans une investigation de bug ou une analyse de cause racine.

Questions fréquentes

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

Est-il interdit d'avaler une exception inattendue avec catch (Exception) pour continuer ?
Se contenter de journaliser puis de continuer est généralement dangereux, car cela masque la cause tout en prolongeant la vie d'un état corrompu. Il est acceptable de continuer seulement lorsque trois conditions sont réunies : on peut abandonner l'unité qui a échoué, on peut restaurer l'état partagé, et on peut rendre compte des effets de bord externes. L'axe de décision n'est pas « peut-on catcher cette exception ? » mais « peut-on encore faire confiance à l'état de l'application après coup ? ».
Quelles exceptions doivent entraîner un arrêt immédiat ?
Les exceptions qui remettent en question la santé de tout le processus, comme StackOverflowException, AccessViolationException, ou un OutOfMemoryException sévère, sont plus sûres à ne pas considérer comme récupérables. StackOverflowException signifie que la pile d'appels s'est effondrée, et AccessViolationException indique un accès illégal à une zone mémoire protégée, ce qui laisse suspecter une corruption de la mémoire. Les anomalies provenant de callbacks COM, P/Invoke ou de SDK tiers penchent également fortement vers l'arrêt, car leur sécurité est difficile à juger depuis le seul côté managé.
À quelles conditions peut-on laisser l'application continuer après une exception ?
Le principe est que la plupart des conditions suivantes soient réunies : l'unité de l'échec est claire (on sait ce que l'on peut abandonner : une opération, un écran, un job, une connexion), l'état peut être détruit puis recréé, la contamination de l'état partagé ne se propage pas, les effets de bord externes peuvent être expliqués, on peut dire honnêtement à l'utilisateur que « ce traitement a échoué », et un suivi est possible via les journaux ou les métriques. Lorsque la frontière du traitement est claire, comme pour une opération UI ou un job d'import unique, il est parfois possible de continuer. À l'inverse, une mise à jour d'état partagé interrompue en cours de route, une boucle parente, un traitement de démarrage ou une anomalie à la frontière native penchent vers l'arrêt.
Peut-on continuer en mettant Handled = true dans DispatcherUnhandledException de WPF ?
Il est effectivement possible de continuer après une exception non gérée, mais pouvoir continuer et pouvoir continuer en toute sécurité sont deux choses différentes. Des gestionnaires comme AppDomain.UnhandledException ou DispatcherUnhandledException sont utiles comme dernier endroit où consigner l'information, mais ce ne sont pas des points de récupération magiques. Il est plus stable, en définitive, de privilégier une configuration permettant d'enquêter après le plantage, en réunissant les informations sur l'exception, le contexte opérationnel et un chemin de collecte des dumps. C'est particulièrement vrai pour les services de longue durée et les applications de surveillance : il est souvent plus sûr et plus facile à diagnostiquer de planter puis de redémarrer que de survivre à moitié cassé.

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