W3docs

Bonnes pratiques de gestion des exceptions Java

Règles pratiques pour la gestion des exceptions en Java : échouer vite, lancer le bon type, ne jamais avaler les exceptions et journaliser utilement.

Les chapitres précédents ont couvert les mécanismes — try, catch, finally, throw, throws, les classes personnalisées. Celui-ci aborde le côté jugement. Deux programmes peuvent utiliser les mêmes constructs et l'un sera robuste tandis que l'autre sera fragile. La différence tient à un petit ensemble d'habitudes qui deviennent des réflexes avec la pratique.

Échouer vite sur une mauvaise entrée

Si une méthode est appelée avec des arguments qu'elle ne peut pas honorer, lancez immédiatement :

public void send(String to, String body) {
  if (to == null || to.isBlank()) {
    throw new IllegalArgumentException("to must be non-blank");
  }
  if (body == null) {
    throw new NullPointerException("body");
  }
  // ...real work
}

N'essayez pas de « faire de votre mieux » avec null. Le bug est dans le code appelant, et plus l'exception se produit près de lui, plus il est facile à corriger. Objects.requireNonNull(body, "body") est le raccourci standard pour le cas null.

L'habitude opposée — substituer silencieusement des valeurs par défaut pour les entrées manquantes — conduit à des erreurs qui remontent cinq couches plus loin sans aucun indice sur qui a passé quoi.

Lancer le type le plus spécifique qui convient

Exception est rarement la bonne chose à lancer, et RuntimeException seulement quand aucun type plus spécifique ne convient. La bibliothèque standard vous offre un vocabulaire — utilisez-le :

  • Mauvais argument → IllegalArgumentException
  • Argument null → NullPointerException (Objects.requireNonNull)
  • État incorrect → IllegalStateException
  • Opération non supportée → UnsupportedOperationException
  • Index hors limites → IndexOutOfBoundsException
  • Nombre hors limites → ArithmeticException ou IllegalArgumentException

Pour les échecs métier, écrivez une exception personnalisée plutôt que de réutiliser une exception intégrée.

Capturer le type le plus spécifique pour lequel vous avez un plan

La règle symétrique. catch (Exception e) est un mauvais signe. Cela capture les bugs de programmation (NullPointerException, IllegalStateException) et les échecs récupérables (IOException) et les exceptions de bibliothèque inconnues dans un seul panier — et les gère presque toujours de manière identique, ce qui est presque toujours faux.

// Bad — what does this even handle?
try { complex(); }
catch (Exception e) { log("failed"); }

// Better — specific cases get specific responses
try { complex(); }
catch (IOException e)         { retryLater(); }
catch (ParseException e)      { recordCorruptInput(e); }

Quand vous ne savez vraiment pas quoi faire d'une classe d'exception, la réponse est ne la capturez pas. Laissez-la se propager vers un gestionnaire qui sait quoi en faire.

Ne jamais avaler une exception silencieusement

Le pire patron de gestion des exceptions qui soit :

try { doWork(); }
catch (Exception e) { }    // never write this

Quand le bug de production inévitable survient, il n'y a pas de trace de pile, pas de message, pas d'entrée dans le journal — l'échec a tout simplement disparu. Si vous avez vraiment l'intention d'ignorer un échec (rare, mais possible — par exemple, la fermeture d'une ressource sur un chemin de nettoyage), dites-le explicitement :

try { connection.close(); }
catch (IOException ignored) {
  // close-time failure on a cleanup path; original cause already propagating
}

Le nom de variable ignored et le commentaire rendent l'intention visible pour le prochain lecteur.

Journaliser utilement, journaliser une seule fois

Deux défauts de journalisation sont courants :

  • Journaliser sans assez de contextelog.error("failed") ne vous dit rien.
  • Journaliser puis relancer — chaque couche journalise la même exception, et la même trace se retrouve dans le journal cinq fois.

Choisissez une couche qui connaît le plus de contexte (généralement au niveau supérieur : gestionnaire de requêtes, exécuteur de tâches) et journalisez-y avec l'entrée qui a déclenché l'échec. Les couches inférieures doivent se concentrer sur la traduction de l'exception, pas sur sa journalisation.

try {
  userService.activate(id);
} catch (UserNotFoundException e) {
  log.warn("activation failed: no user with id={}", id, e);   // include the exception object as the last arg
  return Response.notFound();
}

Passer l'exception comme dernier argument du logger est la convention SLF4J — cela garantit que la trace de pile complète et toute chaîne de causes se retrouvent dans la sortie.

Préserver la cause lors de l'encapsulation

Quand vous traduisez une exception vers une couche supérieure, passez toujours l'originale comme cause :

// Good — cause is preserved
catch (IOException e) {
  throw new ConfigLoadException("failed to load " + path, e);
}

// Bad — original IOException is lost
catch (IOException e) {
  throw new ConfigLoadException("failed to load " + path);
}

La chaîne Caused by: dans la trace de pile résultante est ce qui permet à l'ingénieur de permanence de remonter d'une exception de domaine jusqu'à l'échec au niveau des octets. La perdre transforme une session de débogage d'une demi-heure en une demi-journée.

Ne pas utiliser les exceptions pour le flux de contrôle

Lancer est coûteux — construire une trace de pile lors de la construction est le coût principal. Plus important encore, cela obscurcit l'intention. Une boucle qui utilise try/catch (NoSuchElementException) pour savoir quand s'arrêter cache ce qu'elle fait :

// Bad
try {
  while (true) {
    process(iter.next());
  }
} catch (NoSuchElementException end) { }

// Good
while (iter.hasNext()) {
  process(iter.next());
}

Quand « non trouvé » est un résultat ordinaire, retournez Optional<T> ou un boolean. Réservez les exceptions à ce qui est réellement exceptionnel.

Garder finally et try-with-resources pour le nettoyage

finally doit libérer des ressources. try-with-resources doit être le choix par défaut pour tout ce qui implémente AutoCloseable. Ne mettez pas de logique métier dans finally — il s'exécute sur les chemins de succès et d'échec sans pouvoir distinguer l'un de l'autre. Et n'utilisez pas return depuis finally — cela supprime silencieusement l'exception originale ou la valeur de retour, ce qui est l'un des bugs les plus difficiles à diagnostiquer.

Documenter ce que vous lancez

Si une méthode peut lancer une exception qui importe aux appelants — vérifiée ou non — dites-le dans le Javadoc :

/**
 * Looks up a user by id.
 *
 * @throws UserNotFoundException if no user with that id exists
 * @throws IllegalArgumentException if id is null or blank
 */
public User lookup(String id) { ... }

Le compilateur impose cela pour les exceptions vérifiées dans la signature. Pour les exceptions non vérifiées, le Javadoc est le seul contrat — et les appelants en ont vraiment besoin quand l'exception affecte la façon dont ils doivent utiliser la méthode.

Utiliser les exceptions pour les échecs, les retours pour les résultats ordinaires

La règle de synthèse, et celle qui relie toutes les autres. Une exception dit quelque chose s'est mal passé et je ne peux pas le régler ici. Une valeur de retour dit voici le résultat. Quand « non trouvé » fait partie du fonctionnement normal, retournez Optional.empty(), un boolean, ou une sentinelle. Quand « la connexion à la base de données a été perdue » survient, lancez.

Le code qui respecte cette distinction est serein : le chemin heureux ressemble à une ligne droite, le chemin inhabituel est dans un bloc différent, et le lecteur peut distinguer l'un de l'autre d'un coup d'œil.

Un exemple concret

Une petite fonction de traitement de commandes qui rassemble les pratiques de ce chapitre — validation fail-fast, types d'exceptions intégrés spécifiques, encapsulation avec cause, et un gestionnaire unique au niveau supérieur qui journalise une seule fois.

java— editable, runs on the server

Quatre appels, quatre chemins différents. Celui qui réussit retourne normalement. Les deux cas IllegalArgumentException (id null, montant nul) sont signalés avec le message qui explique ce qui était incorrect dans l'entrée. L'échec de service simulé remonte comme un OrderProcessingException de domaine avec l'IllegalStateException originale liée via getCause(). Rien n'est avalé, rien n'est journalisé deux fois, et chaque échec indique exactement quelle valeur l'a causé.

Et ensuite

Cela clôt la Partie 8 — vous maîtrisez maintenant les mécanismes d'exception de Java et le jugement pour les utiliser correctement. La prochaine partie explore en profondeur les chaînes de caractères — le type le plus courant dans le code Java de loin, et qui recèle plus de profondeur que sa surface ne le suggère. Continuez vers la classe String Java.

Pratique

Pratique
Laquelle de ces habitudes de gestion des exceptions est **la plus** susceptible de rendre le débogage en production douloureux ?
Laquelle de ces habitudes de gestion des exceptions est **la plus** susceptible de rendre le débogage en production douloureux ?
Was this page helpful?