Docs LogoDocs
Spring Boot NotesIntermediate

Exception Handling

Centralizing error handling with @RestControllerAdvice.

Exception Handling

Why Centralize It?

Without a global handler, unhandled exceptions leak stack traces (a security risk) and every controller ends up with repetitive try/catch blocks. Spring's @ControllerAdvice centralizes this in one place.

Custom Exceptions

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

public class DuplicateResourceException extends RuntimeException {
    public DuplicateResourceException(String message) {
        super(message);
    }
}

Global Handler with @RestControllerAdvice

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse body = new ErrorResponse("NOT_FOUND", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        Map<String, String> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
                .collect(Collectors.toMap(FieldError::getField,
                        fe -> fe.getDefaultMessage(), (a, b) -> a));
        ErrorResponse body = new ErrorResponse("VALIDATION_ERROR", "Invalid request", fieldErrors);
        return ResponseEntity.badRequest().body(body);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
        // log the full exception server-side, never return internal details to the client
        ErrorResponse body = new ErrorResponse("INTERNAL_ERROR", "Something went wrong");
        return ResponseEntity.internalServerError().body(body);
    }
}
public record ErrorResponse(String code, String message, Map<String, String> details) {
    public ErrorResponse(String code, String message) {
        this(code, message, null);
    }
}

Order of Resolution

@ExceptionHandler methods are matched by the most specific exception type first. Keep a catch-all Exception handler last as a safety net — but never let it leak stack traces or internal messages to the client.

@ResponseStatus on Custom Exceptions (simpler alternative)

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException { ... }

Fine for very simple cases, but a @ControllerAdvice gives you a consistent response body shape across all error types — usually the better long-term choice for a real API.

Logging Practice

Log the exception with context (request path, user id if available) at the point where it's handled, at WARN for expected errors (404, validation) and ERROR for unexpected ones — don't log-and-rethrow at every layer, which creates duplicate noisy log entries.

Last updated on July 15, 2026

On this page