Exception Handling in Spring Boot

2 minute read

A professional API returns consistent, structured error responses — not stack traces. Spring Boot’s @ControllerAdvice gives you centralized exception handling.


Custom Exceptions

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String resource, Long id) {
        super(resource + " not found with id: " + id);
    }
}

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

public class BusinessException extends RuntimeException {
    private final String errorCode;

    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() { return errorCode; }
}

@ControllerAdvice — Global Exception Handler

@RestControllerAdvice
public class GlobalExceptionHandler {

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

    @ExceptionHandler(DuplicateResourceException.class)
    public ResponseEntity<ErrorResponse> handleDuplicate(DuplicateResourceException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.CONFLICT.value(),
            "Conflict",
            ex.getMessage(),
            LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        Map<String, String> fieldErrors = new LinkedHashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(err ->
            fieldErrors.put(err.getField(), err.getDefaultMessage())
        );

        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Validation Failed",
            "Request validation failed",
            LocalDateTime.now(),
            fieldErrors
        );
        return ResponseEntity.badRequest().body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        // Log the full stack trace
        log.error("Unexpected error", ex);

        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Internal Server Error",
            "An unexpected error occurred",    // don't expose details
            LocalDateTime.now()
        );
        return ResponseEntity.internalServerError().body(error);
    }
}

Error Response DTO

public record ErrorResponse(
    int status,
    String error,
    String message,
    LocalDateTime timestamp,
    Map<String, String> fieldErrors
) {
    public ErrorResponse(int status, String error, String message, LocalDateTime timestamp) {
        this(status, error, message, timestamp, null);
    }
}

Response example:

{
  "status": 404,
  "error": "Not Found",
  "message": "Book not found with id: 42",
  "timestamp": "2026-04-17T10:30:00",
  "fieldErrors": null
}

ProblemDetail (RFC 7807) — Spring Boot 3+

Spring Boot 3 natively supports the standard error format:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND, ex.getMessage()
        );
        problem.setTitle("Resource Not Found");
        problem.setType(URI.create("https://api.bookstore.com/errors/not-found"));
        problem.setProperty("timestamp", Instant.now());
        return problem;
    }
}

Response:

{
  "type": "https://api.bookstore.com/errors/not-found",
  "title": "Resource Not Found",
  "status": 404,
  "detail": "Book not found with id: 42",
  "instance": "/api/books/42",
  "timestamp": "2026-04-17T10:30:00Z"
}

Enable globally:

spring:
  mvc:
    problemdetails:
      enabled: true

Using Exceptions in Services

@Service
public class BookService {

    public Book findById(Long id) {
        return bookRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Book", id));
    }

    public Book create(CreateBookRequest request) {
        if (bookRepository.existsByIsbn(request.isbn())) {
            throw new DuplicateResourceException(
                "Book with ISBN " + request.isbn() + " already exists"
            );
        }
        Book book = new Book(request.title(), request.author(), request.isbn(), request.price());
        return bookRepository.save(book);
    }
}

Key Takeaways

  1. @RestControllerAdvice centralizes all exception handling in one place.
  2. Create custom exceptions for different error types.
  3. Return consistent error responses — never expose stack traces.
  4. Use ProblemDetail (RFC 7807) for standards-compliant APIs.
  5. Always log unexpected exceptions but return generic messages to clients.

What’s Next?

Next: Swagger / OpenAPI — auto-generating API documentation.

Updated: