Spring Boot Global Exception Handling Explained: @ControllerAdvice, @RestControllerAdvice & @ExceptionHandler with Examples
Exception handling is one of the most important aspects of building a robust REST API in Spring Boot. Every application encounters unexpected situations, such as invalid input, missing…
Exception handling is one of the most important aspects of building a robust REST API in Spring Boot. Every application encounters unexpected situations, such as invalid input, missing resources, database failures, or internal server errors. If these exceptions are not handled properly, Spring Boot returns default error responses that expose technical details and provide a poor experience for API consumers.
A well-designed exception handling mechanism makes your APIs more secure, easier to maintain, and more user-friendly. Instead of exposing stack traces, you can return meaningful error messages with appropriate HTTP status codes in a consistent format.
Spring Boot provides three powerful annotations for centralized exception handling:
@ControllerAdvice@RestControllerAdvice@ExceptionHandler
In this tutorial, you’ll learn each annotation in detail with practical examples, execution flow, and best practices used in real-world Spring Boot applications.
Why Do We Need Global Exception Handling?
Consider a simple REST API that retrieves user information.
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService service;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return service.getUser(id);
}
}
Now suppose the requested user does not exist.
The service throws an exception.
throw new UserNotFoundException("User not found");
Without any exception handling, Spring Boot returns its default error response.
{
"timestamp": "2026-06-15T12:30:00",
"status": 500,
"error": "Internal Server Error",
"trace": "Very long stack trace..."
}
This response has several problems:
- Exposes internal implementation details
- Difficult for frontend developers to understand
- Inconsistent API responses
- Poor user experience
Instead, we should return a clean response like this:
{
"status": 404,
"message": "User not found",
"timestamp": "2026-06-15T12:30:00"
}
This is where Spring Boot’s global exception handling becomes useful.
What is @ControllerAdvice?
@ControllerAdvice is a Spring annotation that allows you to define global exception handling logic for all controllers in your application.
Instead of writing try-catch blocks inside every controller method, you write exception handling code once in a single class.
Think of it as a centralized error management system.
Without @ControllerAdvice
UserController
├── try-catch
ProductController
├── try-catch
OrderController
├── try-catch
EmployeeController
├── try-catch
With @ControllerAdvice
UserController
ProductController
OrderController
EmployeeController
│
â–¼
GlobalExceptionHandler
All controllers share the same exception handler.
Problem Without @ControllerAdvice
Imagine the following controller.
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService service;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
try {
return service.getUser(id);
} catch (UserNotFoundException ex) {
throw ex;
}
}
}
Now every controller starts looking similar.
try {
}
catch(Exception ex){
}
Repeated everywhere.
This creates duplicate code and makes maintenance difficult.
Using @ControllerAdvice
Now let’s remove all exception handling from the controller.
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService service;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return service.getUser(id);
}
}
The controller now contains only business logic.
Exception handling is moved to one separate class.
@ControllerAdvice
public class GlobalExceptionHandler {
}
This class can now handle exceptions thrown from every controller.
What is @ExceptionHandler?
@ExceptionHandler tells Spring which method should execute whenever a specific exception occurs.
General syntax
@ExceptionHandler(ExceptionClass.class)
public ResponseEntity<?> methodName(ExceptionClass ex){
}
Example
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex){
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
}
Whenever UserNotFoundException is thrown, Spring automatically calls this method.
No manual invocation is required.
Complete Working Example
Step 1: Create User Entity
public class User {
private Long id;
private String name;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
// getters and setters
}
Step 2: Repository
@Repository
public class UserRepository {
public User findById(Long id){
return null;
}
}
For demonstration purposes, the repository always returns null.
Step 3: Create Custom Exception
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message){
super(message);
}
}
Step 4: Service Layer
@Service
public class UserService {
@Autowired
private UserRepository repository;
public User getUser(Long id){
User user = repository.findById(id);
if(user == null){
throw new UserNotFoundException(
"User with ID " + id + " not found");
}
return user;
}
}
Notice that the service throws the exception.
It does not handle it.
Step 5: Controller
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService service;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id){
return service.getUser(id);
}
}
Still no try-catch.
Step 6: Global Exception Handler
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex){
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
}
Now every time UserNotFoundException occurs, Spring automatically executes this method.
Complete Request Flow
Client
│
â–¼
UserController
│
â–¼
UserService
│
â–¼
UserRepository
│
â–¼
Database
│
â–¼
User not found
│
â–¼
throw UserNotFoundException
│
â–¼
@ControllerAdvice
│
â–¼
@ExceptionHandler
│
â–¼
ResponseEntity
│
â–¼
Client
Returning Custom JSON Response
Returning only a string is not considered a good REST API practice.
Instead, create an error response object.
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String message;
public ErrorResponse(LocalDateTime timestamp,
int status,
String message) {
this.timestamp = timestamp;
this.status = status;
this.message = message;
}
// getters
}
Update the exception handler.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex){
ErrorResponse response = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.NOT_FOUND.value(),
ex.getMessage()
);
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
}
}
API Response
{
"timestamp": "2026-06-15T12:45:10",
"status": 404,
"message": "User with ID 10 not found"
}
This approach is much cleaner and easier for frontend applications to consume.
Handling Multiple Exceptions
One global exception handler can manage many exceptions.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUser(UserNotFoundException ex){
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<String> handleProduct(ProductNotFoundException ex){
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<String> handleOrder(OrderNotFoundException ex){
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
}
Spring automatically selects the appropriate handler based on the exception type.
Handling All Exceptions
Sometimes unexpected exceptions occur.
Instead of exposing internal server details, handle them globally.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception ex){
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Something went wrong.");
}
}
This serves as a fallback handler for all unhandled exceptions.
What is @RestControllerAdvice?
@RestControllerAdvice is simply a shortcut annotation.
Internally, it combines:
@ControllerAdvice
@ResponseBody
Instead of writing
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
}
You can simply write
@RestControllerAdvice
public class GlobalExceptionHandler {
}
For REST APIs, this is the recommended approach.
Difference Between @ControllerAdvice and @RestControllerAdvice
| Feature | @ControllerAdvice | @RestControllerAdvice |
|---|---|---|
| Used with | Spring MVC Controllers | REST APIs |
| Returns Views | Yes | No |
| Automatically adds @ResponseBody | No | Yes |
| Suitable for JSON APIs | Requires @ResponseBody | Yes |
| Recommended for REST APIs | No | Yes |
Best Practices
- Keep controllers free from
try-catchblocks. - Throw meaningful custom exceptions from the service layer.
- Handle exceptions centrally using
@RestControllerAdvice. - Return structured JSON responses instead of plain text.
- Use appropriate HTTP status codes such as
404,400,401,403, and500. - Avoid exposing stack traces or sensitive internal details in API responses.
- Create a common
ErrorResponseclass for consistent responses across the application.
Frequently Asked Interview Questions
1. What is @ControllerAdvice?
It is a Spring annotation used to define global exception handling, global model attributes, and data binding logic that applies across multiple controllers.
2. What is @RestControllerAdvice?
It is a specialized version of @ControllerAdvice that automatically adds @ResponseBody, making it ideal for REST APIs returning JSON responses.
3. What is @ExceptionHandler?
@ExceptionHandler maps a specific exception type to a method. Whenever that exception is thrown during request processing, Spring invokes the corresponding handler method automatically.
4. Why should we use global exception handling?
Global exception handling:
- Eliminates duplicate
try-catchblocks - Keeps controllers clean
- Provides consistent API responses
- Improves maintainability
- Makes APIs easier for frontend applications to consume
5. Can one @ControllerAdvice handle multiple exceptions?
Yes. A single global exception handler class can contain multiple @ExceptionHandler methods, each handling a different exception type.
Conclusion
Global exception handling is a fundamental part of every Spring Boot REST API. By using @ControllerAdvice, @RestControllerAdvice, and @ExceptionHandler, you can centralize your exception handling logic, keep controllers clean, and return consistent, user-friendly error responses.
In real-world applications, this approach improves code maintainability, enhances API security by preventing stack trace exposure, and provides a better developer experience for frontend teams consuming your APIs.
If you’re preparing for Spring Boot or Java backend interviews, mastering these annotations is essential, as they are among the most frequently asked topics in technical interviews.