Global Exception Handler - VittorioDeMarzi/hero-beans GitHub Wiki

Here’s a concise doc for your @RestControllerAdvice—what it does, how responses are built, and how ErrorMessageModel fits in.


🌐 GlobalExceptionHandler — How it works

What it is

@RestControllerAdvice centralizes error handling for all your REST controllers. Any exception thrown inside a controller (or bubbled up from services) that matches one of your @ExceptionHandler methods will be caught here, so you don’t repeat try/catch logic in each endpoint.


Flow at runtime

  1. A controller/service throws an exception (e.g., EmailOrPasswordIncorrectException).
  2. Spring detects there’s a matching @ExceptionHandler in your GlobalExceptionHandler.
  3. Your handler delegates to buildErrorResponse(...).
  4. You log the error and return a ResponseEntity<ErrorMessageModel> with the right HTTP status.

What your handler does

@ExceptionHandler(
    value = [
        EmailOrPasswordIncorrectException::class,
    ],
)
fun handleForbidden(ex: RuntimeException) =
    buildErrorResponse(HttpStatus.FORBIDDEN, ex)
  • Match: Handles EmailOrPasswordIncorrectException.
  • Status: Returns 403 Forbidden.
  • Delegation: Uses a single builder to keep response shape consistent.
fun buildErrorResponse(
    status: HttpStatus,
    ex: RuntimeException,
): ResponseEntity<ErrorMessageModel> {
    logger.error("Exception caught: ${ex.message}", ex)
    ex.cause?.let { cause ->
        logger.error("Caused by: ${cause.message}", cause)
    }
    val errorMessage = ErrorMessageModel(status.value(), ex.message)
    return ResponseEntity(errorMessage, status)
}
  • Logging: Logs the exception plus its cause (if present) for debugging/observability.
  • Payload: Builds an ErrorMessageModel(status, message) so clients always receive a predictable JSON structure.
  • HTTP Envelope: Wraps the payload in a ResponseEntity with the chosen status.

Error response model

class ErrorMessageModel(
    var status: Int? = null,
    var message: String? = null,
)
  • status: Numeric HTTP status (e.g., 403).
  • message: Human-readable error message (from the exception).

Example JSON returned:

{
  "status": 403,
  "message": "Email or password is incorrect"
}

How to use it in your codebase

  • Throw domain exceptions in your service/controller:

    if (!passwordEncoder.matches(raw, storedHash)) {
        throw EmailOrPasswordIncorrectException("Email or password is incorrect")
    }
    
  • Don’t catch these in controllers—let them bubble up so the advice handles them.

  • Add more handlers by defining additional @ExceptionHandler(...) methods that call buildErrorResponse(...) with appropriate statuses (e.g., BAD_REQUEST, NOT_FOUND, CONFLICT, etc.).


When to add new handlers

  • Authentication/AuthorizationUNAUTHORIZED (401), FORBIDDEN (403)
  • ValidationBAD_REQUEST (400)
  • Missing resourceNOT_FOUND (404)
  • Conflicts/duplicatesCONFLICT (409)
  • Server-side errorsINTERNAL_SERVER_ERROR (500)

This keeps client contracts clear and consistent: same JSON shape, status-specific semantics.


If you want, I can draft a short table mapping your custom exceptions → HTTP status you plan to use, so your team (and frontend) have a single reference.