Skip to Content
03 ImplementationGestion des Exceptions

Gestion des Exceptions

Guide complet pour gérer les erreurs proprement dans votre application Micronaut avec GlobalExceptionHandler.

Une bonne gestion des erreurs améliore l’expérience utilisateur et facilite le débogage. Ce guide vous montre comment créer un système d’exceptions cohérent.


Pourquoi un GlobalExceptionHandler ?

Un GlobalExceptionHandler centralise la gestion des erreurs pour retourner des réponses HTTP cohérentes.

Sans GlobalExceptionHandler

@Get public TrendResponseDto getTrends(@QueryValue String keyword) { try { return getTrendsUseCase.execute(keyword) .map(TrendResponseDto::fromDomain) .orElse(null); // ❌ Retourne null au lieu de 404 } catch (Exception e) { // ❌ Erreur non gérée, retourne 500 avec stack trace exposée e.printStackTrace(); return null; } }

Problèmes :

  • Code répétitif dans chaque controller
  • Pas de format standard pour les erreurs
  • Stack traces exposées au client
  • Codes HTTP incorrects

Architecture de Gestion des Erreurs

Flux de Gestion des Erreurs

┌─────────────────────────────────┐ │ TrendController │ │ @Get │ │ getTrends( │ │ @NotBlank String keyword │ │ ) { │ │ throw ResourceNotFound(...); │ │ } │ └─────────┬────────────┬──────────┘ │ │ │ Validation │ ResourceNotFoundException │ échoue │ ▼ ▼ ┌──────────────┐ ┌────────────────────┐ │ Micronaut │ │ GlobalException │ │ Handler │ │ Handler (custom) │ │ (built-in) │ │ │ │ │ │ Intercepte: │ │ Gère: │ │ - ResourceNotFound │ │ - @NotBlank │ │ - Exception │ │ - @Pattern │ │ │ │ - @Size │ │ Retourne: │ │ │ │ - ErrorResponse │ └──────┬───────┘ └─────────┬──────────┘ │ │ └────────┬───────────┘ ┌────────────────┐ │ Client │ │ Reçoit JSON │ │ + HTTP Status │ └────────────────┘

Créer le GlobalExceptionHandler

Créer l’ErrorResponse DTO

Structure standardisée pour toutes les erreurs.

// application/port/input/dto/ErrorResponse.java package org.smoka.application.port.input.dto; import com.fasterxml.jackson.annotation.JsonInclude; import io.micronaut.http.HttpStatus; import lombok.Builder; import lombok.Data; import java.time.LocalDateTime; @Data @Builder @JsonInclude(JsonInclude.Include.NON_NULL) public class ErrorResponse { private int status; // 400, 404, 500, etc. private String error; // "Bad Request", "Not Found" private String message; // Message utilisateur private LocalDateTime timestamp; private String path; // Endpoint appelé private Object details; // Infos supplémentaires (optionnel) // Factory method simple public static ErrorResponse of( HttpStatus httpStatus, String message, String path ) { return ErrorResponse.builder() .status(httpStatus.getCode()) .error(httpStatus.getReason()) .message(message) .timestamp(LocalDateTime.now()) .path(path) .build(); } // Factory method avec détails public static ErrorResponse of( HttpStatus httpStatus, String message, String path, Object details ) { return ErrorResponse.builder() .status(httpStatus.getCode()) .error(httpStatus.getReason()) .message(message) .timestamp(LocalDateTime.now()) .path(path) .details(details) .build(); } }

Créer ResourceNotFoundException

Exception custom pour les ressources introuvables (404).

// application/exception/ResourceNotFoundException.java package org.smoka.application.exception; public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } public ResourceNotFoundException(String resourceName, Object id) { super(String.format("%s with id '%s' not found", resourceName, id)); } }

Créer le GlobalExceptionHandler

// infrastructure/exception/GlobalExceptionHandler.java package org.smoka.infrastructure.exception; import io.micronaut.context.annotation.Requires; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Produces; import io.micronaut.http.server.exceptions.ExceptionHandler; import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; import org.smoka.application.exception.ResourceNotFoundException; import org.smoka.application.port.input.dto.ErrorResponse; @Slf4j @Singleton @Produces @Requires(classes = {Exception.class, ExceptionHandler.class}) public class GlobalExceptionHandler implements ExceptionHandler<Exception, HttpResponse<ErrorResponse>> { @Override public HttpResponse<ErrorResponse> handle(HttpRequest request, Exception exception) { // 1. ResourceNotFoundException → 404 if (exception instanceof ResourceNotFoundException) { return handleResourceNotFound(request, (ResourceNotFoundException) exception); } // 2. Toutes autres exceptions → 500 return handleGenericException(request, exception); } private HttpResponse<ErrorResponse> handleResourceNotFound( HttpRequest request, ResourceNotFoundException exception ) { log.info("Resource not found: {}", exception.getMessage()); ErrorResponse error = ErrorResponse.of( HttpStatus.NOT_FOUND, exception.getMessage(), request.getPath() ); return HttpResponse.notFound(error); } private HttpResponse<ErrorResponse> handleGenericException( HttpRequest request, Exception exception ) { log.error("Unhandled exception for {} {}", request.getMethod(), request.getPath(), exception); ErrorResponse error = ErrorResponse.of( HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred. Please contact support if the problem persists.", request.getPath() ); return HttpResponse.serverError(error); } }

Utiliser dans un Controller

@Get public TrendResponseDto getTrends( @QueryValue @NotBlank String keyword, @QueryValue(defaultValue = "US") String region ) { return getTrendsUseCase.execute(keyword, region) .map(TrendResponseDto::fromDomain) .orElseThrow(() -> new ResourceNotFoundException( String.format("No trends found for keyword '%s' in region '%s'", keyword, region) )); }

Types d’Erreurs Gérées

404 - ResourceNotFoundException

Quand l’utiliser : Ressource demandée n’existe pas.

Code :

@Get("/{id}") public TrendResponseDto getTrendById(@PathVariable Long id) { return repository.findById(id) .map(TrendResponseDto::fromDomain) .orElseThrow(() -> new ResourceNotFoundException("Trend", id)); }

Requête :

GET /api/trends/999

Réponse :

{ "status": 404, "error": "Not Found", "message": "Trend with id '999' not found", "timestamp": "2025-10-18T16:30:00", "path": "/api/trends/999" }

Utilisez Optional.orElseThrow() pour transformer un Optional vide en 404 automatiquement.


Créer une Exception Custom

Créer la Classe d’Exception

// application/exception/TrendLimitExceededException.java package org.smoka.application.exception; public class TrendLimitExceededException extends RuntimeException { public TrendLimitExceededException(int limit) { super(String.format( "You have exceeded the limit of %d trend requests per minute", limit )); } }

Ajouter le Handler

@Override public HttpResponse<ErrorResponse> handle(HttpRequest request, Exception exception) { if (exception instanceof ResourceNotFoundException) { return handleResourceNotFound(request, (ResourceNotFoundException) exception); } // ✅ Nouveau handler if (exception instanceof TrendLimitExceededException) { return handleTrendLimitExceeded(request, (TrendLimitExceededException) exception); } return handleGenericException(request, exception); } private HttpResponse<ErrorResponse> handleTrendLimitExceeded( HttpRequest request, TrendLimitExceededException exception ) { log.warn("Rate limit exceeded for {}", request.getRemoteAddress()); ErrorResponse error = ErrorResponse.of( HttpStatus.TOO_MANY_REQUESTS, // 429 exception.getMessage(), request.getPath() ); return HttpResponse.status(HttpStatus.TOO_MANY_REQUESTS).body(error); }

Utiliser dans le Code

@Get public TrendResponseDto getTrends(@QueryValue String keyword) { if (rateLimiter.isLimitExceeded(request.getClientAddress())) { throw new TrendLimitExceededException(100); } // Logique normale }

Résultat :

{ "status": 429, "error": "Too Many Requests", "message": "You have exceeded the limit of 100 trend requests per minute", "timestamp": "2025-10-18T16:30:00", "path": "/api/trends" }

Mapping Exception → Code HTTP

Voici comment chaque exception est mappée à un code HTTP.

ExceptionCode HTTPGéré parQuand l’utiliser
ResourceNotFoundException404GlobalExceptionHandlerRessource n’existe pas
@NotBlank, @Pattern, @Size400Micronaut (auto)Validation paramètres
TrendLimitExceededException429GlobalExceptionHandlerRate limiting
IllegalArgumentException400GlobalExceptionHandlerArgument invalide
Exception (toutes autres)500GlobalExceptionHandlerErreur inattendue

Règle : Utilisez des exceptions custom pour les erreurs métier. Laissez Micronaut gérer les validations techniques.


Tests (Piège Important)

Piège : Vous ne pouvez PAS tester les erreurs de validation Micronaut car il a ses propres handlers plus spécifiques !

Tests qui NE marcheront JAMAIS

@Test void shouldReturn400WhenKeywordIsMissing() { // ❌ CE TEST NE MARCHERA JAMAIS // Micronaut gère les @QueryValue manquants AVANT ton handler // Ton GlobalExceptionHandler n'est JAMAIS appelé ! HttpClientResponseException ex = assertThrows( HttpClientResponseException.class, () -> client.toBlocking().exchange( HttpRequest.GET("/api/trends") ) ); // ❌ Ton ErrorResponse ne sera jamais retourné // C'est le format Micronaut qui est retourné }

Pourquoi ? Micronaut a une hiérarchie de handlers :

  1. Handlers spécifiques Micronaut → priorité haute
  2. Ton GlobalExceptionHandler → priorité basse

Ce que vous pouvez tester

Type d’erreurGéré parHandler appelé ?Testable ?
@QueryValue manquantMicronaut❌ Non❌ Non
@Valid échoueMicronaut❌ Non❌ Non
JSON mal forméMicronaut❌ Non❌ Non
ResourceNotFoundExceptionVotre handler✅ Oui✅ Oui
Custom exceptionsVotre handler✅ Oui✅ Oui
RuntimeExceptionVotre handler✅ Oui✅ Oui

Bonnes Pratiques

À Faire

1. Utiliser des exceptions custom pour la logique métier

throw new ResourceNotFoundException("Trend", id);

2. Logger les erreurs serveur (500)

log.error("Unexpected error", exception);

3. Masquer les détails techniques en production

String message = "An unexpected error occurred. Please contact support if the problem persists.";

4. Fournir des messages clairs

"Keyword must be at least 2 characters long" // ✅ Clair "Invalid input" // ❌ Trop vague

5. Tester tous les cas d’erreur

@Test void shouldReturn404WhenResourceNotFound() { } @Test void shouldReturn500WhenUnexpectedError() { }

6. Utiliser les annotations de validation Micronaut

@NotBlank(message = "Keyword cannot be empty") @Pattern(regexp = "...", message = "Keyword must contain...")

Structure Finale

      • ResourceNotFoundException.java
      • TrendLimitExceededException.java
      • GlobalExceptionHandler.java

Checklist

Avant de considérer votre gestion d’exceptions complète :

  • ErrorResponse DTO créé avec factory methods
  • ResourceNotFoundException créée et utilisée
  • GlobalExceptionHandler créé et gère 404 + 500
  • Validation avec @NotBlank, @Pattern, @Size ajoutée aux controllers
  • Logs configurés (DEBUG, INFO, ERROR)
  • Messages d’erreur clairs et informatifs
  • Stack traces masquées en production
  • Tests des exceptions custom (avec controller de test)
  • Documentation des codes HTTP retournés

Résumé

Un GlobalExceptionHandler bien configuré améliore la sécurité, la maintenabilité et l’expérience utilisateur de votre API.

Type d’ErreurCode HTTPGéré parUtilisation
Validation400Micronaut@NotBlank, @Pattern, @Size
ResourceNotFoundException404GlobalExceptionHandlerRessource introuvable
TrendLimitExceededException429GlobalExceptionHandlerRate limiting
Exception (autres)500GlobalExceptionHandlerErreurs inattendues

Règle d’or : Utilisez des exceptions custom pour les erreurs métier. Laissez Micronaut gérer les validations simples. Masquez toujours les détails techniques en production.


Prochaines étapes

Maintenant que vous maîtrisez la gestion des exceptions, explorez l’utilisation avancée de ObjectMapper pour les données JSON complexes.

Last updated on