Cours NestJS · Module 6: Gestion des erreurs et monitoring
Custom Exceptions - les situations particulières
Dans cette leçon5
Le filtre de la leçon précédente se tient prêt, mais a-t-il quelque chose à intercepter ? Dans le service se trouve throw new Error('tribut introuvable'). Le filtre reçoit un objet dont il ne sait qu'une chose : que c'est une erreur. Pour reconnaître ce qui s'est passé, il devrait lire le texte du message. Et dès que quelqu'un corrige une faute de frappe dans cette phrase, le traitement cesse de fonctionner.
Un fonctionnaire romain ne décrivait pas une crise par une phrase. Il inscrivait un code d'affaire, une gravité et si l'on peut y remédier sur place - et c'est seulement d'après ces champs que l'on savait qui devait s'en charger. Une exception bien bâtie fonctionne de la même façon.
De quoi hériter
La première décision prête à confusion, car NestJS possède sa propre classe HttpException. Il est tentant de l'utiliser - après tout, elle connaît déjà les codes HTTP.
C'est un piège. HttpException appartient à la couche transport, tandis que tes exceptions décrivent le domaine : un tribut manquant, une limite de légion dépassée, une solde impayée. Si un service levait une HttpException, il saurait que HTTP existe - et tu ne pourrais alors pas l'utiliser dans une tâche par lots ni dans un consommateur de file, où il n'y a aucun HTTP. Rappelle-toi la règle de la leçon précédente : c'est le filtre qui traduit les exceptions internes en HTTP. Le service n'a qu'à dire ce qui s'est passé.
Nous héritons donc d'Error - la classe intégrée de JavaScript. Attention à deux noms qui sonnent vraisemblables mais n'existent pas : NestException et RuntimeException. Cette dernière vient de Java et de C# ; JavaScript n'en a pas.
La classe de base
L'ancêtre commun de toutes nos exceptions ressemble à ceci :
1export abstract class LegionaryException extends Error {
2 constructor(
3 message: string,
4 public readonly code: string,
5 public readonly severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL',
6 public readonly recoverable: boolean = false,
7 public readonly context: Record<string, unknown> = {},
8 ) {
9 super(message);
10 this.name = this.constructor.name;
11
12 Error.captureStackTrace(this, this.constructor);
13 }
14
15 toJSON() {
16 return {
17 name: this.name,
18 code: this.code,
19 severity: this.severity,
20 recoverable: this.recoverable,
21 context: this.context,
22 };
23 }
24}La classe est abstraite, car personne ne devrait la lever directement - elle ne sert que d'ancêtre commun. Grâce à cela, un filtre n'a besoin que de @Catch(LegionaryException) pour attraper d'un coup toutes les exceptions du domaine.
Parcourons les quatre champs, car chacun répond à une question différente :
code est un identifiant d'affaire fixe, par exemple 'TRIBUTE_NOT_FOUND'. C'est par lui que tu reconnais une erreur dans le code et dans les journaux - jamais par le texte du message, qui se traduit et se reformule.
severity dit quelle est la gravité de la situation. Quatre niveaux, du moins au plus critique : LOW pour les problèmes mineurs, MEDIUM pour ceux qui demandent de l'attention, HIGH pour une menace sérieuse, CRITICAL pour une panne critique. C'est d'après ce champ qu'un système d'alerte décide s'il faut écrire une entrée de journal ou réveiller quelqu'un par téléphone.
recoverable répond à la question de savoir si l'erreur peut être réparée automatiquement. Une perte momentanée de la connexion à la base est réparable - il vaut la peine de réessayer. Un tribut manquant ne l'est pas : réessayer n'apporte rien, car une seconde plus tard il ne sera toujours pas là. Ce champ pilote le mécanisme de nouvelles tentatives, pour qu'il ne s'acharne pas sans fin sur ce qui est perdu d'avance.
context porte les données d'accompagnement - l'identifiant du tribut, le nom de la légion, tout ce qui aide à comprendre la situation à la lecture d'un journal. Le type Record<string, unknown> est un objet ordinaire à clés quelconques.
La méthode toJSON() renvoie l'exception sous une forme prête à être écrite dans un journal. Note ce qu'elle ne contient pas : la pile d'appels. C'est délibéré - une pile sert au débogage, mais dans des journaux collectés en production elle occuperait les neuf dixièmes de la place.
captureStackTrace - une trace propre
Une ligne mérite une explication à part :
1Error.captureStackTrace(this, this.constructor);Sans elle, la pile d'appels commencerait à l'intérieur du constructeur de l'exception - un endroit qui ne t'intéresse pas, puisque tu l'as écrit une fois et qu'il fonctionne. Le second argument dit : saute ce constructeur et tout ce qui est au-dessus, commence la trace là où l'exception a réellement été levée.
C'est un détail qui épargne bien des plissements d'yeux à la lecture des journaux. La ligne elle-même ne chiffre rien, n'efface rien et n'envoie rien - elle fixe seulement le début de la trace.
Une exception concrète
L'ancêtre en place, les exceptions particulières sont brèves :
1export class TributeNotFoundException extends LegionaryException {
2 constructor(tributeId: string) {
3 super(
4 `Tribute ${tributeId} not found`,
5 'TRIBUTE_NOT_FOUND',
6 'MEDIUM',
7 false,
8 { tributeId },
9 );
10 }
11}L'ordre est toujours le même : la déclaration de classe, extends LegionaryException, le constructeur recevant les données nécessaires pour décrire l'affaire, et à l'intérieur super(...) qui remplit les champs de l'ancêtre.
Note ce que tu gagnes au point d'utilisation. Au lieu de throw new Error('tribut introuvable ' + id), tu écris throw new TributeNotFoundException(id) - plus court, et le filtre reconnaît le type par instanceof sans lire aucun message. L'identifiant atterrit dans context, le journal dira donc de quel tribut il s'agissait.
Je te le recommande comme règle : une exception par situation du domaine, avec son code et sa gravité fixés une fois, dans le constructeur. Le service n'a alors plus de décision à prendre - tu lèves la bonne classe et tu passes à la suite.
Récapitulatif
Les crises portent des codes d'affaire, pas des phrases :
- les exceptions du domaine héritent d'
Error- la classe intégrée de JavaScript, - pas d'
HttpException, qui appartient à la couche transport et ferait connaître HTTP au service ;NestExceptionetRuntimeExceptionn'existent pas du tout, - c'est le filtre qui traduit une exception en code HTTP - l'exception dit seulement ce qui s'est passé,
- la classe de base est abstraite, si bien que
@Catch(LegionaryException)attrape toute la famille, codeest un identifiant d'affaire fixe - reconnais les erreurs par lui, jamais par le texte du message,severitydu moins au plus critique : LOW → MEDIUM → HIGH → CRITICAL,recoverabledit si l'erreur peut être réparée automatiquement - il pilote les nouvelles tentatives,contextporte les données utiles à la lecture d'un journal,toJSON()prépare une entrée sans la pile d'appels,Error.captureStackTrace(this, this.constructor)fait commencer la trace au point de levée, en sautant le constructeur - il ne chiffre ni n'efface rien,- une exception concrète : la classe,
extendsla classe de base, un constructeur,super(...)avec le code et la gravité.
Dans la prochaine leçon, nous suivrons ces exceptions plus loin - comment les consigner dans la chronique des échecs pour qu'on puisse en relire quelque chose. En attendant, retiens ceci : une exception est un formulaire d'affaire avec un code, une gravité et une note - et non une phrase que quelqu'un reformulera un jour.
Code de cette leçon : src/exceptions/custom-exceptions.ts
1// Custom Exceptions - Specjalne Sytuacje Rzymskie
2// Hiérarchie d'exceptions personnalisées pour l'Imperium
3import { HttpException, HttpStatus } from '@nestjs/common';
4
5// ===========================================
6// 1. Exception de base de l'Imperium
7// ===========================================
8
9export abstract class LegionaryException extends HttpException {
10 public readonly code: string;
11 public readonly severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
12 public readonly recoverable: boolean;
13
14 constructor(
15 code: string,
16 message: string,
17 status: HttpStatus,
18 severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' = 'MEDIUM',
19 recoverable: boolean = true,
20 ) {
21 super(
22 {
23 code,
24 message,
25 severity,
26 recoverable,
27 timestamp: new Date().toISOString(),
28 },
29 status,
30 );
31 this.code = code;
32 this.severity = severity;
33 this.recoverable = recoverable;
34 }
35}
36
37// ===========================================
38// 2. Exceptions spécifiques
39// ===========================================
40
41// Légionnaire introuvable
42export class LegionaryNotFoundException extends LegionaryException {
43 constructor(legionaryId: string) {
44 super(
45 'LEGIONARY_NOT_FOUND',
46 'Legionariusz ' + legionaryId + ' does not exist in the registry!',
47 HttpStatus.NOT_FOUND,
48 'LOW',
49 false,
50 );
51 }
52}
53
54// Permissions insuffisantes
55export class InsufficientRankException extends LegionaryException {
56 constructor(requiredRank: string, currentRank: string) {
57 super(
58 'INSUFFICIENT_RANK',
59 'Wymagana ranga: ' + requiredRank + ', aktualna: ' + currentRank,
60 HttpStatus.FORBIDDEN,
61 'MEDIUM',
62 false,
63 );
64 }
65}
66
67// Tribut épuisé
68export class TributeDepletedException extends LegionaryException {
69 constructor(province: string) {
70 super(
71 'TRIBUTE_DEPLETED',
72 'Prowincja ' + province + ' nie ma wiecej tributow!',
73 HttpStatus.CONFLICT,
74 'HIGH',
75 true,
76 );
77 }
78}
79
80// Erreur de bataille
81export class BattleException extends LegionaryException {
82 constructor(message: string) {
83 super(
84 'BATTLE_ERROR',
85 message,
86 HttpStatus.INTERNAL_SERVER_ERROR,
87 'CRITICAL',
88 false,
89 );
90 }
91}
92
93// ===========================================
94// 3. Exemple d'utilisation
95// ===========================================
96
97function findLegionary(id: string) {
98 const legionaries = ['LEG-001', 'LEG-002', 'LEG-003'];
99
100 if (!legionaries.includes(id)) {
101 throw new LegionaryNotFoundException(id);
102 }
103
104 return { id, name: 'Marcus Aurelius', rank: 'centurion' };
105}
106
107console.log('=== Custom Exceptions ===');
108console.log('LegionaryException - classe de base des exceptions');
109console.log('Champs : code, severity, recoverable, timestamp');
110console.log('Chaque exception a un HttpStatus spécifique');
111console.log('Hiérarchie : base -> exceptions spécifiques');
112Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle classe une classe Custom Exception de base doit-elle étendre ?
2. Quel champ d'une Custom Exception indique si l'erreur peut être récupérée automatiquement ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complète la classe abstraite LegionaryException extends Error avec les champs : code (string), severity ('LOW'|'MEDIUM'|'HIGH'|'CRITICAL'), recoverable (boolean), context (Record) et une méthode toJSON()
- Tri vertical
Ordonne les niveaux de gravité des exceptions du moins au plus critique
- Clique dans l'ordre
Ordonne correctement les éléments de la définition d'une Custom Exception concrète
- Éditeur de code
Complète la classe TributeNotFoundException qui étend LegionaryException et qui, dans le constructor(tributeId), appelle super avec le message, code : 'TRIBUTE_NOT_FOUND' et severity : 'MEDIUM'