When building RESTful APIs with Spring Boot, exception handling becomes critical for providing meaningful responses and avoiding verbose stack traces in the client response. Rather than handling exceptions in every controller, Spring Boot offers a clean, centralized way: @ControllerAdvice
.
In this guide, we’ll explore:
- What is
@ControllerAdvice
- Handling exceptions globally
- Returning structured JSON responses
- Customizing error messages
- Real-world patterns

🔍 What is @ControllerAdvice
?
@ControllerAdvice
is a specialization of @Component
used to define global exception handling logic across the entire application. You can catch, log, and respond to exceptions thrown in your controllers—all in one place.
✅ Setup Spring Boot Project
Ensure your project has the following dependency:
1 2 3 4 5 6 7 |
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> |
Spring Boot’s web starter already configures Jackson and basic exception handling infrastructure.
💥 Example: Throwing an Exception
Let’s create a simple REST controller that might throw an exception:
📂 Controller: UserController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.kscodes.springboot.exceptiondemo.controller; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/users") public class UserController { @GetMapping("/{id}") public String getUser(@PathVariable int id) { if (id <= 0) { throw new IllegalArgumentException("User ID must be greater than 0"); } return "User with ID " + id; } } |
This controller throws IllegalArgumentException
for invalid IDs.
🧰 Global Exception Handling with @ControllerAdvice
Create a global handler class:
📁 GlobalExceptionHandler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package com.kscodes.springboot.exceptiondemo.exception; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity<Map<String, Object>> handleIllegalArgument(IllegalArgumentException ex) { Map<String, Object> errorBody = new HashMap<>(); errorBody.put("timestamp", LocalDateTime.now()); errorBody.put("status", HttpStatus.BAD_REQUEST.value()); errorBody.put("error", "Bad Request"); errorBody.put("message", ex.getMessage()); return new ResponseEntity<>(errorBody, HttpStatus.BAD_REQUEST); } @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, Object>> handleGenericException(Exception ex) { Map<String, Object> errorBody = new HashMap<>(); errorBody.put("timestamp", LocalDateTime.now()); errorBody.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value()); errorBody.put("error", "Internal Server Error"); errorBody.put("message", "An unexpected error occurred"); return new ResponseEntity<>(errorBody, HttpStatus.INTERNAL_SERVER_ERROR); } } |
🔁 Sample Error Response
1 2 3 4 5 6 7 8 9 |
{ "timestamp": "2025-06-28T10:15:30.000", "status": 400, "error": "Bad Request", "message": "User ID must be greater than 0" } |
🧪 Testing the Endpoint
1 2 3 |
curl http://localhost:8080/api/users/0 |
Returns HTTP 400 Bad Request
with the structured JSON error.
🧩 Validating Input with @Valid
+ Exception Handling
Let’s add a POST endpoint and handle validation errors.
📁 User.java
1 2 3 4 5 6 7 8 9 |
public class User { @NotBlank(message = "Name is required") private String name; @Email(message = "Email is invalid") private String email; } |
📁 UserController.java
1 2 3 4 5 6 7 |
@PostMapping public String createUser(@Valid @RequestBody User user) { return "User created: " + user.getName(); } |
➕ Add Handler for Validation Errors
1 2 3 4 5 6 7 8 9 10 |
@ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage())); return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); } |
🔁 Sample Response
1 2 3 4 5 6 7 |
{ "name": "Name is required", "email": "Email is invalid" } |
🧼 Clean Error Model (Optional)
Create a custom error response model:
1 2 3 4 5 6 7 8 9 10 11 |
public class ApiError { private LocalDateTime timestamp; private int status; private String message; private String path; // Getters/Setters/Constructor } |
Modify Handler
1 2 3 4 5 6 7 8 |
@ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ApiError> handleResourceNotFound(ResourceNotFoundException ex, WebRequest request) { ApiError error = new ApiError(LocalDateTime.now(), 404, ex.getMessage(), request.getDescription(false)); return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); } |
🧾 Best Practices
Tip | Description |
---|---|
Use specific exception classes | e.g., UserNotFoundException , InvalidRequestException |
Return standard structure | Helps front-end developers consume error responses consistently |
Avoid exposing internals | Never return full stack trace or DB error directly |
Log the errors | Use SLF4J/Logback to log details for debugging |
Use @ResponseStatus for simple use cases | On custom exceptions if JSON body is not needed |
📚 External References
✅ Conclusion
Using @ControllerAdvice
, you can manage exceptions in a centralized and consistent way, improving API design and maintainability. With structured responses and specific handlers, your applications become more robust and developer-friendly.