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
- A controller/service throws an exception (e.g.,
EmailOrPasswordIncorrectException
). - Spring detects there’s a matching
@ExceptionHandler
in yourGlobalExceptionHandler
. - Your handler delegates to
buildErrorResponse(...)
. - 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 callbuildErrorResponse(...)
with appropriate statuses (e.g.,BAD_REQUEST
,NOT_FOUND
,CONFLICT
, etc.).
When to add new handlers
- Authentication/Authorization →
UNAUTHORIZED (401)
,FORBIDDEN (403)
- Validation →
BAD_REQUEST (400)
- Missing resource →
NOT_FOUND (404)
- Conflicts/duplicates →
CONFLICT (409)
- Server-side errors →
INTERNAL_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.