Exception Handling in Spring Boot
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
@RestControllerAdvicecentralizes all exception handling in one place.- Create custom exceptions for different error types.
- Return consistent error responses — never expose stack traces.
- Use ProblemDetail (RFC 7807) for standards-compliant APIs.
- Always log unexpected exceptions but return generic messages to clients.
What’s Next?
Next: Swagger / OpenAPI — auto-generating API documentation.