Retour au cours

frontend / javascript

Gestion d'erreurs avancée

Leçon 161 exercice

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 catch avec instanceof
  • 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 catch qui 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'erreurReprésenteRéaction typique côté appelant
ValidationErrorDonnée d'entrée invalideAfficher un message au formulaire
AuthenticationErrorUtilisateur non authentifiéRediriger vers la page de connexion
NotFoundErrorRessource introuvableAfficher 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.

js
// --- 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 Error avec 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

1 disponible
1

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 :

js
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.

Résoudre l’exercice →