frontend / javascript
Gestion d'erreurs avancée
Explication
Ce que vous allez apprendre
- Créer des classes d'erreur personnalisées héritées de
Error - Distinguer le type d'une erreur dans un
catchavecinstanceof - Chaîner une erreur à sa cause d'origine avec
Error.cause - Choisir entre lever une exception et retourner un objet
{ succes, valeur } - Reconnaître et corriger un
catchqui avale une erreur silencieusement
Dans quel contexte ?
En production, un service de paiement échoue silencieusement : aucune erreur dans les logs, mais les paiements n'arrivent jamais à destination. Un développeur finit par trouver un catch (e) {} vide autour de l'appel à l'API bancaire, ajouté "temporairement" pour faire taire une erreur pendant un test, jamais retiré depuis. Ce genre de catch silencieux est l'une des pires pratiques en gestion d'erreurs : il transforme un problème visible et corrigeable en un dysfonctionnement invisible et bien plus coûteux à diagnostiquer.
1. Les erreurs sont inévitables, il faut les structurer
Toute application finit par rencontrer des situations anormales : une donnée invalide, une ressource introuvable, un utilisateur non authentifié. La question n'est pas de les éviter — c'est impossible — mais de les représenter de façon structurée, pour que le code appelant sache comment réagir.
2. Créer ses propres classes d'erreur
La technique centrale consiste à créer ses propres classes d'erreur, héritées de la classe native Error, chacune représentant une catégorie précise de problème (validation, authentification, ressource introuvable).
| Classe d'erreur | Représente | Réaction typique côté appelant |
|---|---|---|
ValidationError | Donnée d'entrée invalide | Afficher un message au formulaire |
AuthenticationError | Utilisateur non authentifié | Rediriger vers la page de connexion |
NotFoundError | Ressource introuvable | Afficher une page 404 |
Prérequis
Cette leçon suppose que l'héritage de classe avec extends (leçon 6) est bien acquis : une classe d'erreur personnalisée est simplement une classe qui extends Error.
3. Pourquoi c'est mieux qu'un simple message texte
Cela permet, dans un catch, de distinguer précisément le type d'erreur rencontré avec instanceof, plutôt que d'analyser un message texte fragile pour deviner ce qui a échoué.
4. Ne jamais perdre la cause d'origine
Une erreur cache souvent une autre erreur plus profonde, comme une configuration illisible parce que le fichier JSON était mal formé. Error.cause permet de chaîner explicitement une erreur à sa cause d'origine sans la perdre en cours de route.
5. Toutes les erreurs ne se valent pas
Il faut aussi distinguer les erreurs "attendues" (un email invalide dans un formulaire) des erreurs vraiment exceptionnelles. Pour les premières, retourner un objet { succes, valeur } plutôt que de lever une exception évite la lourdeur des exceptions pour des cas qui font partie du fonctionnement normal.
6. La règle à ne jamais enfreindre
Ne jamais avaler silencieusement une erreur inattendue dans un catch : soit on sait la gérer, soit on la relance pour qu'elle remonte à quelqu'un qui peut agir.
Piège fréquent
try { ... } catch (e) {} (ou pire, catch (e) { console.log("erreur"); } sans aucune action) fait disparaître le problème des radars sans jamais le résoudre. Un catch doit toujours soit traiter réellement l'erreur, soit la relancer (throw e;) après journalisation.
Commandes & code
Gestion d'erreurs avancée
Structurer et propager les erreurs de façon professionnelle.
// --- Hierarchie d'erreurs personnalisees ---
class ErreurApplicative extends Error {
constructor(message, { code, statut = 500, details } = {}) {
super(message);
this.name = this.constructor.name; // "ErreurApplicative" ou nom de la sous-classe
this.code = code;
this.statut = statut;
this.details = details;
Error.captureStackTrace?.(this, this.constructor); // exclut ce constructeur de la stack trace
}
}
class ErreurValidation extends ErreurApplicative {
constructor(message, champs) {
super(message, { code: "VALIDATION_ERROR", statut: 400, details: champs });
}
}
class ErreurAuthentification extends ErreurApplicative {
constructor(message = "Non authentifie") {
super(message, { code: "AUTH_ERROR", statut: 401 });
}
}
class ErreurRessourceIntrouvable extends ErreurApplicative {
constructor(ressource, id) {
super(`${ressource} avec l'id ${id} introuvable`, { code: "NOT_FOUND", statut: 404 });
}
}
function validerUtilisateur(donnees) {
const erreurs = {};
if (!donnees.email?.includes("@")) erreurs.email = "Email invalide";
if (!donnees.age || donnees.age < 0) erreurs.age = "Age invalide";
if (Object.keys(erreurs).length > 0) {
throw new ErreurValidation("Donnees utilisateur invalides", erreurs);
}
}
try {
validerUtilisateur({ email: "pas-un-email", age: -5 });
} catch (erreur) {
if (erreur instanceof ErreurValidation) {
console.log(erreur.statut, erreur.details); // 400 { email: "...", age: "..." }
} else {
throw erreur; // erreur inattendue : re-lancer, ne pas avaler silencieusement
}
}
// --- Error.cause (ES2022) : chainer une erreur en preservant sa cause originale ---
async function chargerConfiguration() {
try {
return JSON.parse(await fetch("/config.json").then((r) => r.text()));
} catch (erreurOriginale) {
throw new Error("Impossible de charger la configuration", { cause: erreurOriginale });
}
}
try {
await chargerConfiguration();
} catch (erreur) {
console.error(erreur.message);
console.error("Cause racine :", erreur.cause?.message);
}
// --- try/catch avec async/await : capturer aussi bien sync que async ---
async function operationRisquee(input) {
if (typeof input !== "string") {
throw new TypeError("Une chaine est attendue"); // erreur SYNCHRONE
}
const reponse = await fetch(`/api/${input}`); // erreur potentiellement ASYNCHRONE
if (!reponse.ok) throw new ErreurRessourceIntrouvable("Item", input);
return reponse.json();
}
// --- Gestion centralisee des erreurs non catchees ---
window.addEventListener("unhandledrejection", (event) => {
console.error("Promise rejetee non geree :", event.reason);
event.preventDefault(); // empeche l'affichage par defaut dans la console
});
window.addEventListener("error", (event) => {
console.error("Erreur globale non geree :", event.error);
});
// --- Pattern Result : alternative aux exceptions pour les erreurs "attendues" ---
function diviser(a, b) {
if (b === 0) {
return { succes: false, erreur: "Division par zero" };
}
return { succes: true, valeur: a / b };
}
const resultat = diviser(10, 0);
if (!resultat.succes) {
console.log("Erreur geree sans exception :", resultat.erreur);
}
// --- Retry avec gestion d'erreur fine selon le type ---
async function appelApiResilient(url) {
try {
return await fetch(url).then((r) => r.json());
} catch (erreur) {
if (erreur instanceof TypeError) {
console.log("Erreur reseau, nouvelle tentative...");
return appelApiResilient(url); // simplifie : ajouter un compteur en production
}
throw erreur; // erreur non recuperable : propager
}
}Résumé
- Étendre
Erroravec des classes métier (code,statut,details) structure la gestion d'erreur à grande échelle. Error.cause(ES2022) préserve la cause originale à travers plusieurs couches d'abstraction.window.addEventListener("unhandledrejection", ...)attrape les Promises rejetées jamais catchées.- Le pattern "Result" (
{ succes, valeur/erreur }) est une alternative aux exceptions pour les erreurs attendues.
Exercices pratiques
Mission : retrouver la cause d'une configuration illisible
Objectif : Distinguer des erreurs métier avec instanceof, chaîner une cause d'erreur, et corriger un catch qui avale silencieusement le problème.
Contexte
Chez Technologik, chargerConfiguration() échoue en production avec un message vague : "Impossible de charger la configuration", sans qu'on sache pourquoi. Le code actuel :
async function chargerConfiguration() {
try {
return JSON.parse(await fetch("/config.json").then((r) => r.text()));
} catch (erreur) {
throw new Error("Impossible de charger la configuration");
}
}La vraie cause (un JSON mal formé, ou une erreur réseau) disparaît complètement. Ailleurs dans le code, un catch (e) {} vide fait aussi disparaître une erreur d'authentification sans laisser aucune trace dans les logs.