Spring Boot Exception Handling
Exception handling controls what your API sends back when something goes wrong. Without proper handling, Spring exposes raw Java stack traces to the client — a security risk and a poor experience. Structured error handling returns clean, useful JSON responses.
The Default Spring Error Response
Without any configuration, Spring returns this when an exception occurs:
{
"timestamp": "2024-05-01T10:00:00.000+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/users/999"
}
This is generic and exposes little detail. You want something like this instead:
{
"status": 404,
"message": "User with ID 999 not found",
"timestamp": "2024-05-01T10:00:00"
}
Step 1 — Create a Custom Exception
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User with ID " + id + " not found");
}
}
Step 2 — Create an Error Response DTO
public class ErrorResponse {
private int status;
private String message;
private String timestamp;
// Constructor, getters
}
Step 3 — Create a Global Exception Handler
@ControllerAdvice intercepts exceptions thrown from any controller in your app:
@RestControllerAdvice ← Applies to all controllers
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(
UserNotFoundException ex
) {
ErrorResponse error = new ErrorResponse(
404,
ex.getMessage(),
LocalDateTime.now().toString()
);
return ResponseEntity.status(404).body(error);
}
@ExceptionHandler(Exception.class) ← Catch-all for unexpected errors
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
ErrorResponse error = new ErrorResponse(
500,
"An unexpected error occurred",
LocalDateTime.now().toString()
);
return ResponseEntity.status(500).body(error);
}
}
How Exception Flow Works
Client sends GET /users/999
│
▼
UserController.getUserById(999)
│
▼
UserService.findById(999)
│
▼
User not found → throw new UserNotFoundException(999)
│
▼ (Spring intercepts)
GlobalExceptionHandler.handleUserNotFound(ex)
│
▼
Returns: HTTP 404 with ErrorResponse JSON
│
▼
Client receives clean error message
@ExceptionHandler on a Single Controller
Handle exceptions only within one controller by placing the method inside that controller (no @ControllerAdvice needed):
@RestController
@RequestMapping("/products")
public class ProductController {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<String> handleNotFound(ProductNotFoundException ex) {
return ResponseEntity.status(404).body(ex.getMessage());
}
}
Handling Validation Errors
When a request body fails validation (covered in the Validation topic), Spring throws MethodArgumentNotValidException:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(
MethodArgumentNotValidException ex
) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(errors);
}
Response example:
{
"email": "must be a valid email address",
"name": "must not be blank"
}
Exception Handling Hierarchy
Priority (highest to lowest) ──────────────────────────────────────────── @ExceptionHandler inside the controller ← Most specific @ExceptionHandler in @ControllerAdvice ← Application-wide Default Spring error handling ← Fallback
Summary
- Use
@RestControllerAdvicewith@ExceptionHandlerto handle errors globally - Create custom exception classes with meaningful messages
- Return a structured
ErrorResponseDTO instead of raw stack traces - Always include a catch-all handler for unexpected
Exceptiontypes - Validation errors throw
MethodArgumentNotValidException— handle it separately
