La grande salle de la taverne est calme.
Sur la table centrale, une carte du donjon est déployée.
Les aventuriers ne discutent pas de gloire.
Ils discutent de survie.
Chaque couloir est étudié.
Chaque salle est analysée.
Chaque piège est anticipé.
Car une chose est certaine :
le danger n’est pas dans ce que l’on voit…
mais dans ce que l’on n’a pas prévu.
En développement, les erreurs suivent exactement la même règle.
Elles ne sont pas exceptionnelles. Elles sont inévitables.
La vraie question devient alors :
avons-nous une carte claire… ou avançons-nous à l’aveugle ?
Différence avec Spring Boot : une philosophie différente
Dans un univers familier comme Spring Boot, la gestion des erreurs est centralisée très tôt.
Un simple @ControllerAdvice, quelques @ExceptionHandler, et l’on obtient :
- un point unique de gestion
- plusieurs méthodes dédiées par type d’erreur
- une structure homogène facile à imposer
@ExceptionHandler(TavernNotFoundException.class)
public ResponseEntity<ProblemDetail> handleNotFound(...) { ... }un handler classic de spring
- Une seule classe
- Plusieurs cas
- Une vision centralisée
C’est une approche confortable, presque naturelle :
un maître de guilde qui gère toutes les situations depuis un seul endroit
Avec Quarkus, le choix est différent.
Basé sur JAX-RS, il repose sur :
ExceptionMapper- une classe par exception
- une résolution automatique par type
@Provider
public class TavernNotFoundExceptionMapper
implements ExceptionMapper<TavernNotFoundException> {
}mapper d'une exception dans Quarkus
- Une exception = un mapper
- Pas de point central unique
- Une approche plus modulaire
Ce n’est pas une limitation.
C’est un changement de philosophie :
chaque aventurier connaît son rôle, plutôt qu’un seul chef qui décide de tout
Problème initial : une exception = un mapper
Au début, les aventuriers notent les dangers comme ils peuvent.
Un parchemin pour les pièges mécaniques.
Un autre pour les malédictions.
Un troisième pour les embuscades.
Mais rien n’est standardisé.
Dans notre API, c’est exactement ce qu’il se passe.
Le service est propre :
public Tavern getTavern(String name) {
return repository.findByName(name)
.orElseThrow(() -> new TavernNotFoundException(name));
}extrait de notre service
Les erreurs sont identifiées.
Mais leur gestion ne l’est pas.
Chaque mapper construit sa réponse :
return Response.status(404)
.entity("Not found")
.build();Et rapidement :
- les formats divergent
- les informations varient
- la duplication s’installe
chaque aventurier note les pièges… mais personne ne dessine la carte
Solution : éviter la duplication et structurer
Un silence s’installe autour de la table.
Puis quelqu’un propose une idée simple :
et si on arrêtait de noter les pièges…
pour commencer à dessiner une carte ?
Un format unique : Problem
On commence par définir une représentation unique.
Un symbole pour chaque piège.
Une structure claire.
Une lecture immédiate.
public class Problem {
private String type;
private String title;
private int status;
private String detail;
private Map<String, Object> additional;
}la structure commune qui sera renvoyée
Toutes les erreurs parlent le même langage.
Le client sait toujours à quoi s’attendre.
la carte commence à prendre forme
Une base métier : BusinessException
Plutôt que de décrire les pièges après coup, on les définit dès le départ.
public abstract class BusinessException extends RuntimeException {
private final int status;
private final String title;
}l'abstraction de nos exceptions
Puis :
public class TavernNotFoundException extends BusinessException {
public TavernNotFoundException(String name) {
super(404, "Ressource introuvable",
"La taverne '" + name + "' n'existe pas.");
}
}une exception
Chaque piège devient :
- identifié
- catégorisé
- documenté
les dangers ne sont plus subis, ils sont connus
Centraliser avec AbstractExceptionMapper
La carte ne suffit pas.
Encore faut-il une méthode pour la dessiner.
public abstract class AbstractExceptionMapper<T extends Throwable>
implements ExceptionMapper<T> {
protected abstract int status(T ex);
protected abstract String title(T ex);
protected String detail(T ex) {
return ex.getMessage();
}
protected void enrich(Problem problem, T ex) {}
public Response toResponse(T ex) {
Problem problem = Problem.of(status(ex), title(ex))
.detail(detail(ex));
enrich(problem, ex);
return Response.status(problem.getStatus())
.type("application/problem+json")
.entity(problem)
.build();
}
}Désormais :
- la structure est centralisée
- les règles sont communes
- les variations sont contrôlées
la carte est dessinée avec une seule encre
Des mappers métier ultra simples
Chaque aventurier reprend sa place.
Mais cette fois, avec une méthode commune.
@Provider
public class TavernNotFoundExceptionMapper
extends AbstractExceptionMapper<TavernNotFoundException> {
protected int status(TavernNotFoundException ex) {
return ex.getStatus();
}
protected String title(TavernNotFoundException ex) {
return ex.getTitle();
}
}un de nos mapper
- Lisible
- Sans duplication
- Axé métier
Enrichir quand c’est utile
Certains pièges nécessitent plus de détails.
Une salle trop remplie.
Un passage bloqué.
Une capacité atteinte.
protected void enrich(Problem problem, TavernCapacityReachedException ex) {
problem.additional(Map.of(
"currentCapacity", ex.getCurrentCapacity(),
"maxCapacity", ex.getMaxCapacity()
));
}dire ce qui est nécessaire
On fournit des informations utiles au client.
Sans casser le format.
Un filet de sécurité global
Même la meilleure carte ne peut tout prévoir.
Alors on ajoute une dernière règle :
@Provider
public class GlobalExceptionMapper
extends AbstractExceptionMapper<Throwable> {
}Toutes les erreurs sont couvertes.
Aucun comportement imprévisible.
même l’inconnu a sa place sur la carte
Une approche alternative : centraliser avec le pattern matching
Alors que la carte commence à prendre forme, un des aventuriers propose une autre stratégie.
Plutôt que de multiplier les rôles, pourquoi ne pas désigner un seul stratège chargé de tout interpréter ?
Un registre unique.
Une lecture centralisée.
Et une analyse des pièges basée sur leur nature.
En Java moderne, cela peut se traduire par l’utilisation du pattern matching :
return switch (ex) {
case BusinessException be -> buildProblem(be);
case ConstraintViolationException ve -> buildValidationProblem(ve);
default -> buildTechnicalProblem(ex);
};on centralise
L’idée est séduisante :
- une seule entrée
- une logique centralisée
- une lecture immédiate des cas
Et sur de petits systèmes, cela fonctionne très bien.
un seul maître de guilde qui lit la carte et donne les ordres
Mais à mesure que le donjon se complexifie, la carte s’alourdit.
De nouveaux pièges apparaissent :
- des règles métier spécifiques
- des enrichissements particuliers
- des cas techniques imprévus
Et le registre central devient progressivement plus dense, plus fragile.
Chaque nouveau piège oblige à modifier ce point unique.
Chaque oubli peut avoir des conséquences inattendues.
le stratège devient indispensable… et donc vulnérable
Une approche plus équilibrée consiste alors à combiner les deux mondes :
- garder une base commune (
BusinessException,Problem) - centraliser ce qui est générique
- déléguer ce qui est spécifique à des mappers dédiés
Le pattern matching garde alors sa place, mais de manière ciblée :
- pour enrichir certains cas proches
- pour factoriser sans sur-centraliser
la carte reste lisible… sans dépendre d’un seul homme
@Provider
public class BusinessExceptionMapper
extends AbstractExceptionMapper<BusinessException> {
@Override
protected int status(BusinessException ex) {
return ex.getStatus();
}
@Override
protected String title(BusinessException ex) {
return ex.getTitle();
}
@Override
protected void enrich(Problem problem, BusinessException ex) {
switch (ex) {
case TavernCapacityReachedException e -> problem.additional(Map.of(
"currentCapacity", e.getCurrentCapacity(),
"maxCapacity", e.getMaxCapacity()
));
default -> {
// rien
}
}
}
}un mapper dédié pour les BusinessException
Cette approche hybride permet de conserver :
- la lisibilité d’un système centralisé
- la robustesse d’un système modulaire
Et surtout, elle respecte un principe essentiel :
dans un donjon complexe, mieux vaut une équipe bien organisée… qu’un seul héros surchargé
Conclusion
Les aventuriers replient la carte.
Le donjon n’a pas changé.
Les pièges sont toujours là.
Mais quelque chose est différent.
Chaque danger est identifié.
Chaque réaction est prévue.
Chaque situation a une réponse.
Avec Quarkus, la gestion des erreurs demande plus de rigueur qu’avec Spring Boot.
Moins de magie, plus de structure.
Mais cette discipline apporte un avantage décisif :
un système clair, maîtrisé, et fidèle à ton métier.
Et dans un donjon comme dans une API…
c’est souvent ce qui fait toute la différence.
Tout le code relatif a cet article est consultable ici :