cursus.

Cours 4 · Robustesse et modélisationLeçon 1 sur 2

Exceptions

5 h de lecture9 sections Version PDF

À la fin de cette leçon, vous saurez

Erreurs et exceptions, try, catch, finally, propagation et throws, exceptions personnalisées, et quand lever plutôt que rendre un code d'erreur.

Comment une méthode signale-t-elle qu'elle n'a pas pu faire son travail ?

La réponse du C était le code de retour : -1 en cas d'échec, et un appelant discipliné qui le teste. Le cours de programmation a montré ce que cela coûte — scanf rend le nombre de conversions réussies, et personne ne le regarde jamais.

int retirer(double montant) {    if (montant > solde) return -1;      /* échec */    solde -= montant;    return 0;} compte.retirer(1000);                    /* et voilà. Personne n'a rien vérifié. */

Quatre défauts, indépendants les uns des autres. Le code d'erreur peut être ignoré en toute impunité — c'est le cas ci-dessus. Il occupe la valeur de retour, donc une méthode qui doit rendre un résultat utile n'a plus de place pour signaler l'échec. Il ne porte aucune information : -1 ne dit pas combien il manquait. Et il doit être propagé à la main à travers chaque niveau d'appel, chacun devant le tester et le retransmettre.

Les exceptions règlent les quatre.

Le mécanisme

public void retirer(double montant) {    if (montant <= 0) {        throw new IllegalArgumentException("montant non positif : " + montant);    }    if (montant > solde) {        throw new SoldeInsuffisantException(montant - solde);    }    solde -= montant;}

throw interrompt immédiatement la méthode. La valeur de retour n'est jamais produite : il n'y a plus de code d'erreur à ignorer, et le chemin d'échec est séparé du chemin normal.

Du côté de l'appelant :

try {    compte.retirer(1000);    System.out.println("retrait effectué");} catch (SoldeInsuffisantException e) {    System.out.println("il manque " + e.getManquant() + " euros");} catch (IllegalArgumentException e) {    System.out.println("montant invalide : " + e.getMessage());}

Ce qui se passe alors mérite d'être décrit précisément, car c'est le mécanisme du bloc I d'Algorithmique 2 employé à l'envers. Quand une exception est levée, la machine remonte la pile d'appels — elle dépile les cadres un par un — jusqu'à trouver un catch capable de la traiter. Tous les cadres traversés sont abandonnés. Si aucun catch ne se présente, la remontée atteint main, le programme s'arrête, et la trace affichée est exactement le contenu de la pile au moment du throw.

D'où la propriété centrale : l'exception franchit les niveaux intermédiaires sans qu'ils aient un mot à dire. Une méthode qui ne sait pas traiter un échec n'a rien à écrire du tout — c'est ce qui supprime la propagation manuelle du code d'erreur.

L'ordre des catch compte : ils sont examinés dans l'ordre, et le premier compatible gagne. Un catch (Exception e) placé avant les autres les rend inatteignables, et le compilateur le refuse — c'est la substituabilité du chapitre 6 appliquée aux erreurs, puisque toutes les exceptions héritent d'un ancêtre commun.

finally

FileReader f = null;try {    f = new FileReader("donnees.txt");    /* lecture */} catch (IOException e) {    System.err.println("lecture impossible");} finally {    if (f != null) f.close();        /* exécuté DANS TOUS LES CAS */}

Le bloc finally s'exécute quoi qu'il arrive : sortie normale, exception attrapée, exception non attrapée qui traverse — et même après un return. C'est le seul endroit où l'on peut garantir qu'une ressource sera rendue.

Java offre depuis la version 7 une forme plus sûre, le try avec ressources, qui appelle close() automatiquement :

try (FileReader f = new FileReader("donnees.txt")) {    /* lecture */} catch (IOException e) { ... }

C'est la forme à employer. Elle évite l'oubli du close, le null à tester, et l'exception que close() pourrait lever dans le finally — cas tordu qui masquerait l'exception d'origine.

Un piège à connaître : un return dans le finally écrase celui du try, exception comprise. C'est légal, c'est déroutant, et cela ne s'écrit pas.

Contrôlées ou non contrôlées

Java est le seul grand langage à faire cette distinction, et elle structure tout le chapitre.

Contrôlées (checked)Non contrôlées (unchecked)
Héritent deExceptionRuntimeException
Le compilateurexige de les traiter ou de les déclarerne dit rien
ExemplesIOException, SQLExceptionNullPointerException, IllegalArgumentException
Signifientun échec prévisible et extérieurune erreur de programmation

Une méthode qui peut lever une exception contrôlée sans la traiter doit l'annoncer :

public void charger(String chemin) throws IOException { ... }

Le critère de choix, tel qu'on l'enseigne : une exception contrôlée signale une situation anormale mais attendue, sur laquelle l'appelant peut agir — fichier absent, réseau coupé. Une exception non contrôlée signale un bogue : un argument invalide, un pointeur nul, un indice hors bornes. On ne demande pas à l'appelant de traiter un bogue, on le corrige.

La distinction est débattue : elle produit des signatures encombrées et pousse à écrire des catch vides pour faire taire le compilateur. C'est pourquoi les langages venus après Java — C#, Kotlin — ne l'ont pas reprise. Il faut la connaître parce que Java l'impose, et savoir qu'elle n'est pas une évidence.

Exceptions personnalisées

On en écrit une dès que le type standard ne dit pas assez, ou qu'on veut transporter une donnée.

public class SoldeInsuffisantException extends RuntimeException {    private final double manquant;     public SoldeInsuffisantException(double manquant) {        super("il manque " + manquant + " euros");        this.manquant = manquant;    }     public double getManquant() { return this.manquant; }}

Trois choix se lisent dans ces lignes. Le nom finit par Exception, convention universelle. Le message passe au parent par super(...), et sera affiché dans la trace. Et la classe porte une donnée — le montant manquant — que l'appelant peut exploiter : c'est le troisième défaut du code d'erreur, réglé.

Le choix du parent — Exception ou RuntimeException — est le vrai choix de conception, et il découle du tableau précédent.

Les trois anti-usages

Ils sont fréquents, et chacun annule le bénéfice du chapitre.

Avaler l'exception.

try { ... } catch (Exception e) { }        /* le pire code de ce cours */

L'échec disparaît sans trace. Le programme continue dans un état inconnu, et le diagnostic devient impossible. Si l'on ne sait vraiment pas quoi faire, on laisse remonter — c'est gratuit, il suffit de ne rien écrire.

Employer les exceptions comme structure de contrôle. Sortir d'une boucle par une exception, ou tester l'existence d'une clé en attrapant l'échec, fonctionne et coûte cher : construire une exception implique de capturer la pile d'appels. Une exception doit rester exceptionnelle.

Attraper trop large. catch (Exception e) ramasse tout, y compris ce qu'on n'avait pas prévu — un bogue de programmation qu'il aurait mieux valu voir. On attrape le type le plus précis possible.

Quiz · vérifiez votre compréhension Sans réponse

Quels défauts du code de retour les exceptions corrigent-elles ?

Quiz · vérifiez votre compréhension Sans réponse

Quand faut-il faire hériter son exception de RuntimeException plutôt que d'Exception ?

À vous

L'exercice suit le fil de bibliothèque, et compare les deux approches sur le même service.

D'abord la version à code de retour : vous écrivez emprunter qui rend -1, -2, 0, puis un appelant qui oublie de tester — et vous constatez qu'un livre est emprunté deux fois sans que rien ne le dise.

Puis la version à exceptions, avec une exception personnalisée transportant la donnée utile. Vous observerez la remontée de pile sur trois niveaux d'appel : le niveau intermédiaire n'écrit rien, et l'exception le traverse.

Enfin les pièges : l'ordre des catch, le finally qui s'exécute même après un return, et le catch vide — que vous instrumenterez pour voir combien d'échecs il a fait disparaître.

Exercice · JavaScript · à vous de jouer

Opposez code de retour et exceptions sur le même service, puis mesurez ce qu'un catch vide fait disparaître.

En attente
// ── 1. Version à code de retour ───────────────────────────────────────────
const OK = 0, DEJA_EMPRUNTE = -1, INCONNU = -2;

class BibliothequeCodes {
  constructor(livres) { this.livres = livres; }
  emprunter(titre, qui) {
    const l = this.livres.find((x) => x.titre === titre);
    if (!l) return INCONNU;
    if (!l.disponible) return DEJA_EMPRUNTE;
    l.disponible = false; l.par = qui;
    return OK;
  }
}

// ── 2. Version à exceptions ───────────────────────────────────────────────
class LivreIndisponibleException extends Error {
  constructor(titre, detenteur) {
    super("« " + titre + " » est déjà emprunté par " + detenteur);
    this.name = "LivreIndisponibleException";
    // ← à écrire : transporter la donnée utile (le détenteur actuel)
  }
}

class BibliothequeExceptions {
  constructor(livres) { this.livres = livres; }
  emprunter(titre, qui) {
    const l = this.livres.find((x) => x.titre === titre);
    if (!l) throw new Error("titre inconnu : " + titre);
    // ← à écrire : lever LivreIndisponibleException si déjà emprunté
    l.disponible = false; l.par = qui;
  }
}

// Trois niveaux d'appel : le niveau intermédiaire n'écrit RIEN.
function servirUnLecteur(biblio, titre, qui) { traiterDemande(biblio, titre, qui); }
function traiterDemande(biblio, titre, qui) { biblio.emprunter(titre, qui); }

// ── 3. Le catch vide, instrumenté ─────────────────────────────────────────
let avales = 0;
function catchVide(action) {
  try { action(); } catch (e) { avales++; }        // le pire code du cours
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Appelez emprunter() deux fois SANS tester le code de retour, et
//    constatez ce qui n'est pas dit.
// 2. Complétez l'exception personnalisée et son levée. Vérifiez que le
//    niveau intermédiaire n'a rien eu à écrire.
// 3. Faites vingt emprunts dont plusieurs impossibles, à travers catchVide :
//    combien d'échecs ont disparu, et qu'en sait le programme ?

const catalogue = () => [
  { titre: "Dune", disponible: true, par: null },
  { titre: "Ubik", disponible: true, par: null },
];
const b = new BibliothequeCodes(catalogue());
b.emprunter("Dune", "Ana");
b.emprunter("Dune", "Bo");
console.log("   état : " + JSON.stringify(b.livres[0]));

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 7 · sur machine

Signaler l'erreur là où on sait la traiter

Choisir entre code de retour et exception, distinguer vérifiées et non vérifiées, et supprimer de son code les deux fautes qui rendent une erreur invisible.

3 h
Avant de commencer
  • Les TP 1 à 6
  1. 1. Sans exceptions

    Écrivez le chargement d'un catalogue en signalant les erreurs par des codes de retour. Comptez les lignes de traitement d'erreur par rapport aux lignes utiles.

  2. 2. Avec exceptions

    Réécrivez la même chose avec des exceptions. Recomptez, et repérez où le code utile est devenu lisible d'un seul tenant.

  3. 3. Vérifiée ou non

    Écrivez une exception vérifiée et une non vérifiée. Appelez les deux sans les traiter et comparez ce que dit le compilateur.

  4. 4. Le catch vide

    Attrapez une exception et ne faites rien. Provoquez l'erreur et observez ce que voit l'utilisateur. Puis affichez la trace et comparez.

  5. 5. Attraper trop large

    Attrapez le type le plus général possible autour d'un bloc, et faites-y survenir une erreur de programmation. Constatez ce qui est masqué.

  6. 6. L'exception qui perd sa cause

    Attrapez une exception basse et relancez-en une métier SANS transmettre la cause. Comparez la trace obtenue avec celle où la cause est transmise.

  7. 7. Fermer quoi qu'il arrive

    Ouvrez un fichier, provoquez une erreur avant la fermeture, et vérifiez que la ressource fuit. Corrigez avec la fermeture automatique.

  8. 8. Choisir

    Pour six situations que vous listerez, décidez : code de retour, exception vérifiée, ou non vérifiée. Justifiez chaque choix en une phrase.

C'est réussi quand
  • Votre version à exceptions sépare visiblement le cas nominal du traitement d'erreur
  • Vous montrez un catch qui masque une erreur de programmation
  • Votre trace conserve la cause d'origine sur trois niveaux
  • Votre fichier est fermé même quand une exception survient

Ce que la suite en fait

Le chapitre 8 donne la notation pour dessiner ce que les six premiers chapitres ont construit, exceptions comprises : une hiérarchie d'exceptions est une hiérarchie de classes ordinaire, et elle se dessine comme telle.

Le chapitre 9 les fera rencontrer les collections et les fichiers, où elles sont partout : IOException à la lecture, NumberFormatException à la conversion, NoSuchElementException sur une collection vide. C'est là qu'on mesure que les exceptions ne sont pas un ornement mais le mode de signalement de toute la bibliothèque standard.

À retenir

Flashcards · 1 / 5Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 9 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.