2.20

Voir en anglais

2.20 Gestion des erreurs et motifs de résilience

Vue d’ensemble et motivation

Chaque programme que vous écrivez échouera. Un disque se remplit, un réseau tombe, un service dépasse son délai, un appelant passe des données invalides, une dépendance renvoie quelque chose que la documentation n’a jamais mentionné. La question n’est jamais si l’échec se produit ; c’est si votre code rencontre cet échec avec un plan ou avec une surprise. La gestion des erreurs est l’art de décider, ligne par ligne et fonction par fonction, ce que fait votre code quand le monde ne coopère pas. C’est la partie la moins glamour de la construction et la partie qui décide, plus que toute fonctionnalité, si les gens font confiance à votre système.

Ce chapitre porte sur la résilience au niveau du code et des composants : les choix à l’intérieur d’une fonction, d’un module, ou d’une API. Il complète le chapitre 3.5, qui couvre la résilience au niveau du système (équilibrage de charge, réplication, basculement à travers les services). Le chapitre 3.5 garde toute la plateforme debout quand une région s’éteint ; ce chapitre empêche une seule requête de corrompre vos données ou de disparaître sans laisser de trace. Les deux se renforcent mutuellement. Un disjoncteur dans votre architecture signifie peu si le code derrière lui avale les exceptions, et une fonction défensive ne peut pas vous sauver si le système environnant n’a aucune redondance. Ce chapitre s’appuie aussi sur le chapitre 2.9 (construction logicielle), où gérer les erreurs était une discipline parmi d’autres ; ici, cela devient le sujet entier.

Pour les grandes équipes, la cohérence est le prix à gagner. Quand des centaines d’ingénieurs gèrent les erreurs de centaines de façons différentes, chaque service devient un puzzle et chaque incident une excavation. Dans les contextes d’entreprise, cette incohérence augmente le coût de chaque audit et de chaque intégration. Dans l’administration publique et d’autres systèmes à forts enjeux, les enjeux sont plus tranchants : la correction, l’échec sûr, et une piste d’audit claire ne sont pas des fonctionnalités que vous ajoutez plus tard mais des propriétés que le système doit avoir dès le premier commit. Un système de prestations qui calcule mal silencieusement, ou un système de registres qui perd un échec sans le journaliser, n’est pas simplement buggé. Il est indigne de confiance d’une façon qui érode l’institution derrière lui.

Principes clés

  • Distinguez les fautes, les erreurs et les échecs, et gérez chacun à la bonne couche.
  • Choisissez échec rapide ou échec sûr délibérément, selon le contexte, jamais par accident.
  • Rendez le contrat de gestion d’erreur de chaque fonction et API explicite et honnête.
  • Validez aux frontières ; faites confiance à l’intérieur ; défendez-vous sans paranoïa.
  • N’avalez jamais silencieusement une erreur ; faites-la remonter, enveloppez-la, ou gérez-la délibérément.
  • Rendez les nouvelles tentatives sûres avec l’idempotence, les délais d’expiration, le recul et la gigue.
  • Donnez au chemin d’erreur la même attention de conception qu’au chemin heureux.

Recommandations

Distinguer les erreurs, les fautes et les échecs

Un vocabulaire négligé produit une gestion négligée, donc commencez par des termes clairs. Une faute est un défaut dans le système : un bug, une mauvaise configuration, une dépendance qui est en panne. Une erreur est l’état interne incorrect qu’une faute produit : un nul là où une valeur devrait être, un solde qui ne se réconcilie plus. Un échec est ce que l’observateur extérieur voit : la requête renvoie la mauvaise réponse, ou aucune réponse. Une faute peut causer de nombreuses erreurs, et de nombreuses erreurs peuvent être attrapées avant que l’une ne devienne un échec visible. Tout le sens de la gestion des erreurs est de briser cette chaîne, d’attraper l’erreur avant qu’elle ne devienne un échec que l’utilisateur ou l’auditeur subit.

Ce vocabulaire vous dit aussi où agir. Les fautes sont traitées en relecture, en test, et en configuration. Les erreurs sont traitées à l’exécution par les motifs de ce chapitre. Les échecs sont traités par l’observabilité (chapitre 9.2) et par la résilience au niveau système du chapitre 3.5. Quand votre équipe partage ces mots, les revues d’incident deviennent plus précises : vous pouvez dire précisément où la chaîne aurait dû être brisée et ne l’a pas été, plutôt que de débattre de ce qu’était « le bug ».

Choisir échec rapide ou échec sûr selon le contexte

L’échec rapide signifie s’arrêter au moment où quelque chose ne va pas, refuser de continuer sur un mauvais état pour que le problème remonte bruyamment et près de sa cause. L’échec sûr signifie se dégrader vers un état connu et inoffensif et continuer à servir ce que vous pouvez servir en toute sécurité. Aucun des deux n’est universellement juste, et la compétence est de choisir selon le contexte. Pendant le développement et aux frontières internes, l’échec rapide est votre ami : un programme qui s’arrête sur un invariant violé vous remet une courte trace d’appel au lieu d’un long mystère. En production, aux bords d’un système orienté utilisateur, l’échec sûr gagne souvent : un panneau de recommandation qui ne renvoie rien vaut mieux qu’une page de paiement qui ne se charge pas.

Décidez cela délibérément pour chaque frontière et consignez la décision par écrit. Un composant de contrôle de vol ou de dispositif médical échoue sûrement vers un état défini parce que continuer sur des données corrompues pourrait blesser quelqu’un. Une écriture de grand livre échoue rapidement parce que poster une mauvaise entrée est pire que n’en poster aucune. Le mauvais appariement est dangereux dans les deux sens : l’échec sûr là où vous aviez besoin de l’échec rapide cache la corruption, et l’échec rapide là où vous aviez besoin de l’échec sûr transforme un accroc cosmétique en panne.

Choisir votre mécanisme de signalement d’erreur et l’utiliser de façon cohérente

Les langages vous donnent deux grandes façons de signaler que quelque chose s’est mal passé. La gestion d’exceptions lance un objet en remontant la pile d’appels jusqu’à ce qu’un gestionnaire l’attrape, séparant le chemin d’erreur de la logique principale. L’alternative est des valeurs d’erreur explicites : la fonction renvoie à la fois un résultat et une erreur, et l’appelant doit inspecter les deux. De nombreux langages modernes formalisent cette dernière approche avec un type Résultat, souvent appelé Result ou Either, qui force l’appelant à déballer soit un succès soit un échec avant d’utiliser la valeur. Chaque approche a un coût. Les exceptions gardent le chemin heureux propre mais peuvent cacher le flux de contrôle et tenter les développeurs vers des blocs attrape-tout qui effacent l’information. Les résultats explicites rendent chaque échec visible dans la signature de type mais ajoutent de la cérémonie et peuvent être ignorés si le langage ne force pas la vérification.

La bonne réponse concerne moins quel mécanisme que la cohérence et l’honnêteté. Choisissez l’idiome que favorisent votre langage et votre écosystème, et appliquez-le uniformément à travers vos services pour qu’un lecteur sache toujours comment voyage l’échec. Réservez les exceptions à des conditions vraiment exceptionnelles, pas à un flux de contrôle ordinaire comme « utilisateur non trouvé », qui est mieux modélisé comme un résultat normal. Quel que soit votre choix, ne laissez jamais un échec devenir invisible : une valeur d’erreur non vérifiée est aussi dangereuse qu’un bloc catch vide. Dans une grande base de code, une convention écrite plus un linter qui signale les erreurs ignorées bat la préférence de tout individu.

Rendre le contrat de gestion d’erreur explicite

Chaque fonction et chaque API a un contrat de gestion d’erreur, que quelqu’un l’ait consigné ou non. Il répond : qu’est-ce qui peut mal tourner ici, comment allez-vous l’apprendre, et qu’est-ce qui est garanti sur l’état quand cela arrive ? Rendez ce contrat explicite. Documentez quelles erreurs une fonction peut renvoyer ou lancer, distinguez les erreurs récupérables (l’appelant peut raisonnablement réessayer ou se rabattre) des irrécupérables (l’appelant ne peut pas corriger cela et devrait propager ou abandonner), et énoncez si la fonction laisse l’état inchangé en cas d’échec. Cette dernière propriété, parfois appelée la garantie forte d’exception, signifie qu’un appel échoué est comme s’il ne s’était jamais produit, ce qui est exactement ce qui permet à un appelant de réessayer en toute sécurité.

Pour une API publique ou inter-équipe, ce contrat fait partie de l’interface, aussi réel que les types de paramètres. Concevez une petite taxonomie d’erreurs stable : un ensemble borné de catégories telles qu’erreur de validation, non trouvé, conflit, non autorisé, dépendance indisponible, et erreur interne. Les appelants peuvent alors brancher sur la catégorie sans analyser des chaînes de caractères. Une taxonomie claire rend la gestion d’erreur composable à travers de nombreux services, et elle rend les échecs auditables, parce que chaque échec correspond à un type connu et nommé.

Valider aux frontières et se défendre sans paranoïa

Traitez les données traversant une frontière de confiance (une requête réseau, un fichier, une saisie utilisateur, un message d’un autre service) comme hostiles jusqu’à validation, et validez-les à la frontière, une fois, complètement. C’est de la programmation défensive appliquée avec jugement. À l’intérieur d’un module dont vous avez déjà validé les entrées, des vérifications redondantes à chaque ligne cachent la logique et suppriment les échecs mêmes que vous voudriez voir. La discipline est : défendez-vous fortement aux bords, faites confiance à l’intérieur. Validez la structure, les plages et les invariants là où les données entrent, convertissez-les en types qui rendent les états illégaux irreprésentables, et laissez le code intérieur supposer qu’il travaille avec des données propres.

La paranoïa a un coût réel. Du code étouffé de vérifications de nul et de branches défensives est plus difficile à lire, et pire, il transforme souvent un échec clair en un haussement d’épaules silencieux, renvoyant une valeur par défaut là où il aurait dû lever une alarme. Le défensif qui masque les bugs n’est pas de la sécurité ; c’est de l’ajournement.

Rendre les nouvelles tentatives sûres, bornées et polies

De nombreuses fautes sont transitoires : un blip réseau momentané, un service qui redémarre, une brève contention de verrou. Dans tout système distribué (chapitre 3.3), ces échecs partiels sont le cas normal plutôt que l’exception. Réessayer est la réponse naturelle, mais une boucle de nouvelle tentative naïve est une arme chargée. D’abord, rendez l’opération que vous réessayez idempotente, c’est-à-dire que l’exécuter deux fois a le même effet que l’exécuter une fois. Sans idempotence, une nouvelle tentative après un délai d’expiration peut débiter une carte deux fois ou créer deux enregistrements, parce que vous ne pouvez pas dire si la première tentative a échoué ou si seul son accusé de réception a été perdu. Utilisez des clés d’idempotence pour les écritures pour que le récepteur puisse reconnaître et dédupliquer une répétition.

Deuxièmement, mettez un délai d’expiration sur chaque appel distant pour qu’une dépendance bloquée ne puisse pas vous bloquer. Troisièmement, espacez les nouvelles tentatives avec un recul exponentiel, doublant l’attente après chaque tentative, et ajoutez de la gigue (un petit délai aléatoire) pour que mille clients se rétablissant en même temps ne se synchronisent pas en une ruée qui rabat le service en cours de rétablissement. Quatrièmement, plafonnez le nombre de nouvelles tentatives et le temps total, puis abandonnez gracieusement. Les nouvelles tentatives sans limites, recul, gigue, et idempotence sont l’une des façons les plus courantes qu’un petit accroc devienne une panne auto-infligée.

Ajouter des disjoncteurs, des cloisons et une dégradation gracieuse dans le code

Quand une dépendance est vraiment en panne, la réessayer ne fait que gaspiller l’effort et approfondir le trou. Un disjoncteur surveille le taux d’échec des appels à une dépendance et, une fois que les échecs franchissent un seuil, « s’ouvre » pour échouer immédiatement pendant une période de refroidissement plutôt que d’attendre sur des appels condamnés. Après le refroidissement, il laisse passer un appel d’essai et se referme si la dépendance s’est rétablie. Cela protège à la fois vos appelants (des échecs rapides et prévisibles au lieu de délais d’expiration empilés) et la dépendance en difficulté (de l’air pour se rétablir). Le motif de cloison, nommé d’après les compartiments étanches d’un navire, isole les ressources pour qu’une dépendance saturée ne puisse pas consommer tous les threads ou connexions et couler le processus entier ; vous donnez à chaque dépendance son propre bassin borné.

Ces motifs s’associent à la dégradation gracieuse au niveau du code : quand une dépendance non essentielle est indisponible, renvoyez un résultat réduit mais utile plutôt qu’une erreur. Montrez des données en cache avec une note de péremption, cachez le panneau de personnalisation, mettez l’écriture en file d’attente pour plus tard. C’est le complément local à la résilience au niveau système du chapitre 3.5 : l’architecture fournit la redondance à travers les machines, et votre code fournit un comportement sensé quand une pièce manque.

Envelopper les erreurs de contexte et ne jamais les avaler

Une erreur qui lit « connexion refusée » dix couches au-dessus de où elle s’est produite est presque inutile. Pendant qu’une erreur se propage, enveloppez-la de contexte : ce que vous essayiez de faire, quelle entité ou requête, quelle dépendance, tout en préservant la cause originale pour que la racine ne soit pas perdue. Les bons langages et bibliothèques prennent en charge ce chaînage d’erreur directement. L’objectif est qu’une seule ligne de journal dise à l’ingénieur d’astreinte ce qui a échoué, pendant quelle opération, pour quelle entrée. C’est la matière première pour l’observabilité du chapitre 9.2 et le débogage du chapitre 2.15.

Le péché capital est d’avaler une erreur : un bloc catch vide, une valeur de retour ignorée, un catch qui journalise au niveau debug et continue comme si de rien n’était. Une erreur avalée ne disparaît pas ; elle réapparaît plus tard sous forme de données corrompues ou de défaut inexplicable, maintenant détachée de sa cause. Chaque erreur doit rencontrer l’un de trois destins : la gérer (récupérer ou dégrader), l’envelopper et la propager, ou, au sommet de la pile, la journaliser avec le contexte complet et échouer. Si vous attrapez une erreur et ne faites aucune de ces choses, vous avez choisi de cacher un futur incident à votre futur vous-même.

Compromis : avantages et inconvénients

ApprocheAvantagesInconvénients
ExceptionsChemin heureux propre ; difficile à ignorer si non vérifiéFlux de contrôle caché ; tente vers l’effacement attrape-tout
Valeurs d’erreur explicites / types RésultatÉchec visible dans la signature ; force la gestionPlus de cérémonie ; peut être ignoré sans imposition
Échec rapideFait remonter les bugs bruyamment, près de la causeMauvaise expérience utilisateur si utilisé au bord
Échec sûrContinue à servir ; protège les utilisateurs et les donnéesPeut masquer la corruption si utilisé là où l’échec rapide était nécessaire
Nouvelles tentatives avec reculTraverse les fautes transitoires automatiquementAmplifie la charge et les doubles écritures sans idempotence
DisjoncteurÉchecs rapides ; laisse les dépendances se rétablirÉtat et réglage supplémentaires ; peut masquer un problème persistant
Validation défensive aux frontièresAttrape les mauvaises données tôt, une fois, bruyammentPoussée trop loin, elle encombre la logique et cache les vrais échecs

La tension centrale est entre la visibilité et le bruit. Gérez les erreurs trop discrètement et vous cachez les problèmes jusqu’à ce qu’ils soient coûteux ; gérez-les trop bruyamment et partout, et vous noyez le signal dans la cérémonie et masquez les échecs qui comptent. Résolvez cela par l’emplacement et l’intention. Soyez bruyant et strict aux frontières, là où les mauvaises données et les échecs de dépendance entrent. Soyez discret et confiant à l’intérieur, là où les entrées sont déjà propres. Décidez échec rapide contre échec sûr par frontière et consignez-le par écrit. Le but est un code où chaque échec a exactement un propriétaire clair et un destin clair, et rien ne tombe silencieusement entre les mailles.

Questions à discuter avec votre équipe

  1. Avons-nous une taxonomie d’erreur et une convention de gestion d’erreur partagées à travers nos services, ou chaque équipe improvise-t-elle ? Dans une grande équipe, c’est la différence entre des échecs qui se composent et des échecs qui confondent. Quand un service renvoie un HTTP 500 pour un problème de validation, un autre lance une exception typée, et un troisième renvoie un nul, chaque intégration devient une négociation et chaque incident un exercice de traduction. Apportez des exemples du même échec logique, disons « enregistrement non trouvé », tel qu’il apparaît à travers trois de vos services, et voyez comme ils le signalent différemment. La réponse devrait devenir une norme écrite : un ensemble borné de catégories d’erreur, une façon cohérente de les signaler, et un linter ou une liste de contrôle de relecture qui l’impose. La cohérence ici rapporte à chaque future intégration, audit, et astreinte.

  2. Pour chaque frontière critique, avons-nous choisi échec rapide ou échec sûr délibérément, et le code correspond-il à ce choix ? La plupart des équipes n’ont jamais pris cette décision explicitement, ce qui signifie qu’elle a été prise pour elles par qui a écrit le code en premier, et de façon incohérente. Les considérations concurrentes sont réelles : échouer sûrement garde les utilisateurs servis mais peut laisser la corruption se propager, tandis qu’échouer rapidement protège les données mais peut transformer une panne mineure de dépendance en un échec visible. Apportez votre historique d’incidents et demandez, pour les quelques pires, si le code a échoué de la façon que vous auriez choisie si on vous avait demandé à l’avance. La preuve que vous voulez est une carte de vos frontières avec une étiquette délibérée sur chacune, particulièrement partout où de l’argent, de la sécurité, ou des registres de citoyens sont impliqués. Là où l’étiquette et le code sont en désaccord, vous avez trouvé votre prochaine correction.

  3. Quand avons-nous pour la dernière fois exercé délibérément un chemin d’erreur, et s’est-il comporté comme conçu ? Le chemin d’erreur est généralement le code le moins testé que vous possédez, pourtant c’est là que la confiance se gagne ou se perd, et « nous échouons sûrement » est une affirmation que vous ne pouvez pas étayer si vous ne l’avez jamais observé se produire. Une boucle de nouvelle tentative sans idempotence, un disjoncteur dont le seuil est faux, une exception avalée dans une branche rarement atteinte : ces choses se cachent jusqu’à ce qu’un vrai incident les trouve pour vous. Apportez les résultats d’une injection délibérée de défaillance (une dépendance tuée, un délai d’expiration induit, une charge utile malformée) dans un environnement réaliste. L’action qui suit est de rendre l’injection de défaillance routinière, pour que le comportement de récupération, de dégradation, et d’échec sûr soit vérifié continuellement plutôt qu’espéré. Tout chemin d’erreur que vous n’avez jamais déclenché est une promesse que vous n’avez pas testée.

  4. Lesquelles de nos opérations d’écriture sont idempotentes, et où une nouvelle tentative après un accusé de réception perdu dupliquerait-elle un effet du monde réel comme un paiement ou un enregistrement ? Réessayer est le réflexe de résilience le plus courant et, fait avec négligence, la façon la plus courante qu’un accroc transitoire se transforme en argent ou données dupliqués. Dans une grande équipe, la logique de nouvelle tentative vit souvent dans des clients partagés, des intergiciels, et des services individuels tous à la fois, donc une seule écriture peut être réessayée à plusieurs couches sans que personne ne possède le comportement total. La tension concurrente est que les clés d’idempotence, la déduplication, et les résultats de requête stockés ajoutent du stockage et du code, et les équipes sous pression de livraison les sautent pour des écritures qu’elles supposent à tort être sûres. Apportez un inventaire de vos écritures visibles extérieurement, chacune marquée si elle porte une clé d’idempotence et comment le récepteur reconnaît et déduplique une répétition. Dans les contextes d’entreprise et gouvernementaux, signalez en premier celles qui déplacent de l’argent ou altèrent le registre d’un citoyen, parce qu’un double paiement ou une prestation dupliquée est une constatation d’audit et parfois une exposition juridique, pas simplement un défaut.

  5. Nos délais d’expiration, disjoncteurs et cloisons viennent-ils d’une bibliothèque partagée et testée, ou chaque équipe les code-t-elle à la main ? Ces motifs sont faciles à décrire et faciles à se tromper subtilement : un délai d’expiration manquant, un seuil de disjoncteur qui ne se déclenche jamais, un bassin de connexions dimensionné pour qu’une dépendance lente affame le processus entier. Quand chaque équipe les réimplémente, vous accumulez de nombreuses copies légèrement cassées et aucun endroit unique pour corriger un défaut une fois que vous le trouvez. La considération concurrente est qu’une bibliothèque partagée impose une interface commune et une cadence de mise à niveau, et les équipes avec des runtimes ou des besoins de latence inhabituels peuvent s’en agacer ou la contourner. Apportez une enquête sur combien d’implémentations distinctes de nouvelle-tentative-et-disjoncteur fonctionnent réellement en production, et quels services n’ont encore aucun délai d’expiration sur leurs appels sortants du tout. Pour une grande entreprise ou une agence, une bibliothèque partagée validée donne aussi aux relecteurs de sécurité et aux auditeurs un seul composant à certifier plutôt que des dizaines, ce qui abaisse le coût de chaque relecture.

  6. Si un incident se produisait la nuit dernière, tout ingénieur d’astreinte pourrait-il le tracer depuis une seule ligne de journal, et un auditeur pourrait-il plus tard voir chaque échec que le système a enregistré ? Une erreur enveloppée, catégorisée, et bien journalisée est la différence entre un diagnostic de dix minutes et une excavation de minuit, et une avalée est un futur incident que vous vous êtes caché à vous-même. Dans une grande équipe, les échecs traversent de nombreux sauts de service, donc la valeur vient d’un contexte cohérent et d’identifiants de corrélation qui survivent à ces sauts, pas de la diligence d’une seule équipe. La tension concurrente est le coût et le bruit : journalisez tout et vous noyez le signal et payez pour le stocker ; journalisez trop peu et vous ne pouvez pas reconstruire ce qui s’est passé. Apportez un vrai échec récent et suivez sa trace de bout en bout, notant chaque saut où le contexte a été perdu ou une erreur a été attrapée et jetée. Dans les systèmes régulés et gouvernementaux, traitez cela comme une propriété de conformité, parce qu’un échec inauditable, ou une décision que vous ne pouvez pas expliquer des années plus tard, est une exposition juridique et pas simplement une lacune opérationnelle.

Regard sectoriel

Jeune pousse. Avec une poignée d’ingénieurs et aucune marge à épargner, dépensez votre budget de gestion d’erreur là où un échec vous coûte un client ou vos données : mettez un délai d’expiration sur chaque appel sortant, rendez vos écritures qui déplacent de l’argent idempotentes, et ajoutez une règle de linter contre les erreurs ignorées. Sautez le cadre élaboré ; un type Résultat pour les fonctions de service centrales et une dégradation gracieuse sur les dépendances non critiques achètent la plupart de la sécurité pour quelques jours de travail. Échouez rapidement en développement pour que les bugs remontent bruyamment, et résistez à coder à la main un disjoncteur avant d’avoir réellement une dépendance qui en justifie un.

Petite entreprise. Sans spécialiste de résilience au personnel et avec un budget serré, appuyez-vous sur ce que votre langage, framework, et fournisseur cloud vous donnent déjà plutôt que de construire des motifs à partir de zéro : les files gérées, les nouvelles tentatives côté fournisseur, et les délais d’expiration de bibliothèque couvrent plus que ce que la plupart des équipes attendent. Cadrez la décision comme acheter contre construire, et achetez partout où une dépendance mature gère les nouvelles tentatives, le recul, et l’idempotence pour vous. Concentrez votre attention rare sur la ou les deux frontières où une transaction fausse ou perdue ferait vraiment mal, et assurez-vous qu’elles échouent sûrement et laissent une trace.

Grande entreprise. À travers de nombreuses équipes, le prix à gagner est la cohérence : une taxonomie d’erreur partagée, une bibliothèque commune pour les délais d’expiration, les nouvelles tentatives, les disjoncteurs, et les cloisons, et un linter et une liste de contrôle de relecture qui les imposent dans le pipeline. Alimentez chaque erreur dans une plateforme d’observabilité unifiée avec des identifiants de corrélation pour qu’un échec soit traçable à travers les sauts de service, et standardisez les décisions échec-rapide contre échec-sûr par frontière pour que les audits trouvent un motif documenté et défendable plutôt qu’une dispersion d’habitudes locales. Gouvernez la bibliothèque partagée comme un vrai produit, parce qu’un défaut corrigé une fois là est un défaut corrigé partout.

Gouvernement. La correction, l’échec sûr, et une piste d’audit durable sont des obligations, pas des préférences. Échouez rapidement sur tout invariant violé qui touche l’argent ou l’éligibilité, validez chaque saisie orientée citoyen à la frontière, et écrivez chaque échec dans un journal immuable avec assez de contexte pour qu’une décision puisse être expliquée et révisée des années plus tard. L’approvisionnement et les longues durées de vie de système signifient que les contrats d’erreur doivent être documentés pour que les fonctionnaires puissent maintenir le code longtemps après le départ des auteurs originaux, et tout composant fournisseur doit exposer son comportement d’échec plutôt que de le cacher derrière une interface opaque.

Exemples

Jeune pousse. Une start-up de quatre personnes livre une application qui appelle un fournisseur de paiement tiers et un service d’e-mail. Tôt, elle ajoute une boucle de nouvelle tentative naïve et facture rapidement deux fois un client quand un délai d’expiration masque une charge réussie. La correction enseigne la leçon : ils ajoutent des clés d’idempotence à chaque écriture, mettent un délai d’expiration sur chaque appel sortant, et passent au recul exponentiel avec gigue. Ils adoptent un type Résultat pour les fonctions de service centrales pour que l’échec apparaisse dans la signature, et une règle de linter signale toute erreur ignorée. Quand l’envoi d’e-mail échoue, le paiement se dégrade gracieusement en mettant le message en file d’attente plutôt qu’en bloquant la vente. La discipline coûte quelques jours et leur épargne une classe d’incidents qui aurait coûté bien plus en remboursements et en confiance.

Grande entreprise. Une entreprise logistique mondiale exploite des centaines de services et standardise la gestion d’erreur à travers tous. Chaque service fait correspondre les échecs à une taxonomie partagée (validation, non trouvé, conflit, dépendance indisponible, interne), donc les appelants branchent sur la catégorie plutôt que d’analyser des messages. Une bibliothèque commune fournit des disjoncteurs, des nouvelles tentatives bornées avec recul et gigue, et des bassins de connexion cloisonnés, donc personne ne code à la main ces motifs mal. Chaque erreur est journalisée avec un contexte de corrélation qui alimente la plateforme d’observabilité du chapitre 9.2, donc un ingénieur d’astreinte peut tracer un échec à travers les sauts de service depuis une seule ligne. Parce que la norme est uniforme et imposée dans le pipeline, les ingénieurs se déplacent avec confiance à travers des services inconnus et les auditeurs peuvent voir que chaque échec est enregistré, catégorisé, et traçable.

Gouvernement. Une agence nationale de prestations construit un système d’éligibilité et de paiement où une mauvaise réponse peut refuser à quelqu’un de l’argent pour son loyer ou surpayer depuis le trésor public. La correction et l’échec sûr sont non négociables, donc le code échoue rapidement sur tout invariant financier violé : un calcul qui ne peut pas se réconcilier refuse de poster plutôt que de poster un chiffre faux. Chaque saisie orientée citoyen est validée à la frontière, et les états illégaux sont rendus irreprésentables dans les types de domaine. Chaque échec est écrit dans un journal d’audit immuable avec le contexte complet, satisfaisant l’exigence légale que les décisions soient explicables et révisables des années plus tard. Là où une dépendance non critique comme l’aperçu de document est en panne, le système se dégrade gracieusement pour qu’un agent de dossier puisse quand même traiter la demande. Les nouveaux fonctionnaires héritent d’un code dont les contrats d’erreur sont documentés, donc ils peuvent le maintenir en toute sécurité longtemps après que les auteurs originaux soient partis.

Argumentaire économique : motivations, retour sur investissement et coût total de possession

Le retour sur une gestion d’erreur disciplinée apparaît comme moins d’incidents, des incidents plus courts, et des incidents moins chers. La plupart des pannes de production ne sont pas exotiques ; elles remontent à une exception avalée, un délai d’expiration manquant, une tempête de nouvelles tentatives, ou une frontière qui a fait confiance à des données qu’elle aurait dû valider. Chacune est évitable avec les motifs présentés ici, et chaque incident évité économise non seulement le coût direct du temps d’arrêt mais les coûts cumulés de la réponse d’urgence, de la perte de clients, et de l’investigation. Parce qu’une erreur enveloppée et bien journalisée peut être diagnostiquée en minutes plutôt qu’en heures, le temps moyen de récupération baisse, et le taux d’échec des changements baisse avec lui à mesure que les ingénieurs cessent de craindre le chemin d’erreur.

Le coût d’adoption est modeste et principalement ponctuel. Vous consignez une taxonomie d’erreur, fournissez une bibliothèque partagée pour les nouvelles tentatives et les disjoncteurs pour que les équipes ne les réinventent pas mal, ajoutez des règles de linter contre les erreurs ignorées, et construisez l’habitude de l’injection de défaillance. Le coût de la négligence s’accumule silencieusement : les erreurs avalées s’accrètent en données corrompues coûteuses à démêler, et une gestion incohérente multiplie le coût de chaque intégration et de chaque audit. Dans les contextes régulés et gouvernementaux, un échec inauditable est une exposition de conformité et juridique, pas seulement un problème d’ingénierie. Pour faire valoir cela auprès de la direction, connectez la discipline de gestion d’erreur aux métriques qu’elle surveille déjà : fréquence des incidents, temps moyen de récupération, taux d’échec des changements, et constatations d’audit.

Anti-patterns et pièges

  • Avaler silencieusement : des blocs catch vides et des valeurs de retour ignorées qui transforment un échec en un mystère différé et détaché.
  • Effacement attrape-tout : un catch large qui journalise un message générique et jette l’erreur originale et son contexte.
  • Nouvelle tentative sans idempotence : réexécuter des écritures non idempotentes après un délai d’expiration, facturant deux fois ou dupliquant des enregistrements.
  • Tempêtes de nouvelles tentatives : pas de recul, pas de gigue, et pas de plafond, si bien que les clients se synchronisent et martèlent une dépendance en cours de rétablissement pour la refaire tomber.
  • Pas de délais d’expiration : des appels distants non bornés qui laissent une dépendance bloquée épuiser les threads et geler le processus entier.
  • Exceptions comme flux de contrôle : lancer et attraper pour des résultats ordinaires comme « non trouvé », cachant la logique et ralentissant le code.
  • Paranoïa défensive : des vérifications à chaque ligne qui enterrent la logique et convertissent de vrais échecs en valeurs par défaut silencieuses.
  • Erreurs typées en chaînes : des appelants qui analysent le texte du message d’erreur parce qu’il n’y a pas de taxonomie stable et catégorisée sur laquelle brancher.
  • Échec sûr là où l’échec rapide était nécessaire : continuer sur un état corrompu dans un système où une mauvaise réponse est pire qu’aucune.

Modèle de maturité

  • Niveau 1 (Initier) : La gestion d’erreur est ad hoc et réactive, décidée par développeur. Les blocs catch vides et les retours ignorés sont courants, les nouvelles tentatives sont naïves, les délais d’expiration manquent, et les échecs apparaissent comme des données corrompues ou des défauts mystérieux sans journalisation cohérente.
  • Niveau 2 (Développer) : Les équipes adoptent des pratiques de base, mais de façon incohérente. Les erreurs sont journalisées avec un certain contexte, l’avalage évident est découragé en relecture, et les délais d’expiration et les nouvelles tentatives simples existent, pourtant les conventions varient entre services, l’idempotence est sporadique, et le chemin d’erreur est rarement testé.
  • Niveau 3 (Standardiser) : Une taxonomie d’erreur partagée et une convention de gestion sont documentées et imposées à travers l’organisation. La validation aux frontières, les nouvelles tentatives idempotentes avec recul et gigue, les disjoncteurs, les cloisons, et l’enveloppement d’erreur sont standard, fournis par des bibliothèques communes, et chaque erreur alimente un pipeline d’observabilité unifié.
  • Niveau 4 (Gérer) : Le comportement de gestion d’erreur est mesuré par rapport à des références et contrôlé avec des données. Les taux de nouvelle tentative, les déclenchements de disjoncteur, les comptes de délai d’expiration, les constatations d’erreur avalée provenant de l’analyse statique, le temps moyen de récupération, et le taux d’échec des changements sont suivis par service ; les seuils de disjoncteur et les délais d’expiration sont ajustés à partir de données de latence et d’échec observées plutôt que devinés ; l’injection de défaillance s’exécute selon un calendrier ; et les équipes révisent ces métriques pour attraper les régressions et tenir chaque choix échec-rapide ou échec-sûr à la preuve.
  • Niveau 5 (Orchestrer) : La résilience est intégrée à la planification de livraison et de risque et continuellement améliorée. La taxonomie, les bibliothèques partagées, et les normes évoluent depuis chaque incident, les expériences de chaos et d’injection de défaillance sont routinières, et l’organisation adapte les délais d’expiration, les seuils de disjoncteur, les stratégies de dégradation, et les décisions de frontière à mesure que le trafic, les dépendances, et le tableau de risque changent.

Pistes de réflexion

  1. Où dans votre base de code une erreur est-elle actuellement avalée, et comment sauriez-vous si vous avez tort qu’elle ne l’est pas ?
  2. Lesquelles de vos opérations d’écriture sont idempotentes, et lesquelles s’exécuteraient deux fois si une nouvelle tentative se déclenchait après un accusé de réception perdu ?
  3. « Utilisateur non trouvé » devrait-il être une exception, une valeur d’erreur, ou un résultat normal, et votre équipe répond-elle à cela de façon cohérente ?
  4. Quelle est votre règle réelle pour où la validation a lieu, et pouvez-vous pointer vers une frontière qui fait confiance à des données qu’elle ne devrait pas ?
  5. Comment décidez-vous du seuil et du refroidissement pour un disjoncteur, et comment sauriez-vous que les réglages actuels sont faux ?
  6. Si un auditeur demandait à voir chaque échec que votre système a connu le mois dernier, pourriez-vous le produire, catégorisé et avec contexte ?

Points clés à retenir

  • Distinguez les fautes, les erreurs, et les échecs, et brisez la chaîne avant qu’une erreur interne ne devienne un échec visible.
  • Choisissez échec rapide ou échec sûr délibérément par frontière, et rendez le contrat de gestion d’erreur de chaque fonction explicite.
  • Validez fortement aux frontières de confiance et faites confiance à l’intérieur ; le défensif qui masque les échecs est de l’ajournement, pas de la sécurité.
  • Rendez les nouvelles tentatives sûres avec l’idempotence, les délais d’expiration, le recul exponentiel, et la gigue, et ajoutez des disjoncteurs et une dégradation gracieuse dans le code.
  • Enveloppez les erreurs de contexte, alimentez-les à l’observabilité, et ne les avalez jamais ; chaque erreur doit être gérée, propagée, ou journalisée et remontée.

Références et lectures complémentaires

  • Michael T. Nygard, Release It! Design and Deploy Production-Ready Software
  • Andrew Hunt et David Thomas, The Pragmatic Programmer
  • Steve McConnell, Code Complete: A Practical Handbook of Software Construction
  • Betsy Beyer, Chris Jones, Jennifer Petoff, et Niall Richard Murphy (dir.), Site Reliability Engineering: How Google Runs Production Systems
  • Marc Brooker, « Timeouts, Retries, and Backoff with Jitter », Amazon Builders’ Library
  • Martin Fowler, « CircuitBreaker », martinfowler.com
  • Nassim Nicholas Taleb, Antifragile: Things That Gain from Disorder