How I Handle Global Exception Handling in a Spring Boot Application
Exception handling is one of those things that often starts simple and gradually becomes messy.
At the beginning of a Spring Boot application, it is common to see code like this:
try {
userService.createUser(request);
} catch (Exception e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Something went wrong");
}
This might work for a small application.
But as the application grows, repeating exception handling logic across controllers quickly becomes difficult to maintain.
In this article, I want to share a simple approach I use to structure global exception handling in a Spring Boot application.
The goals are straightforward:
- Keep controllers clean
- Return consistent API responses
- Separate business errors from system errors
- Make the application easier to maintain
The Problem with Handling Exceptions in Every Controller
Imagine an application with many endpoints.
Without centralized exception handling, you might end up with something like this:
@PostMapping("/users")
public ResponseEntity<?> createUser(
@RequestBody CreateUserRequest request) {
try {
User user = userService.createUser(request);
return ResponseEntity.ok(user);
} catch (UserAlreadyExistsException e) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(e.getMessage());
} catch (Exception e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Internal server error");
}
}
Then another controller might contain similar code.
And another.
Over time, this creates several problems:
- Repeated code
- Inconsistent error responses
- Controllers become harder to read
- Exception logic becomes scattered across the application
I prefer to move this responsibility into one centralized place.
1. Start with Custom Exceptions
The first step is to distinguish between different types of errors.
For example:
public class ResourceNotFoundException
extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Another example:
public class BusinessException
extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
For duplicate resources:
public class ResourceAlreadyExistsException
extends RuntimeException {
public ResourceAlreadyExistsException(String message) {
super(message);
}
}
These exceptions make the business logic easier to understand.
For example:
public User findById(Long id) {
return userRepository
.findById(id)
.orElseThrow(() ->
new ResourceNotFoundException(
"User not found"
)
);
}
The service does not need to know anything about HTTP responses.
It simply expresses what went wrong.
2. Create a Consistent Error Response
One thing I strongly recommend is using a consistent error response format.
For example:
{
"timestamp": "2026-08-15T10:30:00",
"status": 404,
"error": "NOT_FOUND",
"message": "User not found",
"path": "/api/users/123"
}
We can represent this with a simple class:
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
// constructors
// getters
}
Using one standard response format makes the API easier to consume.
Clients do not need to handle a completely different error structure for every endpoint.
3. Use @RestControllerAdvice
Spring provides a very useful mechanism for centralized exception handling:
@RestControllerAdvice
public class GlobalExceptionHandler {
}
Now we can define handlers for different exceptions.
For example:
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request) {
ErrorResponse response = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
}
Now whenever a ResourceNotFoundException is thrown anywhere in the application, Spring can handle it automatically.
The controller stays clean.
4. Handle Business Exceptions Separately
Not every exception is a system failure.
Sometimes an error is part of normal business behavior.
For example:
User already exists
Or:
Account is inactive
Or:
Document processing is not allowed
These can be handled separately.
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
BusinessException ex,
HttpServletRequest request) {
ErrorResponse response = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity
.badRequest()
.body(response);
}
This gives the client a predictable response.
5. Handle Validation Errors
Validation errors are another important case.
For example:
public class CreateUserRequest {
@NotBlank
private String username;
@Email
private String email;
@NotBlank
private String password;
}
When validation fails, Spring throws a validation exception.
We can handle it globally:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex,
HttpServletRequest request) {
String message = ex
.getBindingResult()
.getFieldErrors()
.stream()
.map(error ->
error.getField()
+ ": "
+ error.getDefaultMessage()
)
.findFirst()
.orElse("Validation failed");
ErrorResponse response = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
message,
request.getRequestURI()
);
return ResponseEntity
.badRequest()
.body(response);
}
For more complex APIs, you may want to return all validation errors instead of just the first one.
For example:
{
"timestamp": "2026-08-15T10:30:00",
"status": 400,
"error": "BAD_REQUEST",
"message": "Validation failed",
"errors": {
"username": "must not be blank",
"email": "must be a valid email address"
}
}
This can provide a better experience for frontend developers and API consumers.
6. Always Have a Fallback Exception Handler
Finally, I recommend having a handler for unexpected exceptions.
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(
Exception ex,
HttpServletRequest request) {
ErrorResponse response = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),
"An unexpected error occurred",
request.getRequestURI()
);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
However, there is an important detail here.
I generally do not return the full exception message to the client.
For example, this is usually not a good idea:
.body(ex.getMessage());
Unexpected exceptions may contain implementation details or sensitive information.
Instead:
Client receives:
An unexpected error occurred
While the actual exception should be logged internally.
For example:
log.error("Unexpected error", ex);
This keeps internal implementation details away from API clients while still making debugging possible.
7. Keep Controllers Focused on HTTP Requests
With global exception handling in place, controllers become much simpler.
Instead of this:
@PostMapping("/users")
public ResponseEntity<?> createUser(
@RequestBody CreateUserRequest request) {
try {
User user = userService.createUser(request);
return ResponseEntity.ok(user);
} catch (Exception e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Something went wrong");
}
}
We can write:
@PostMapping("/users")
public ResponseEntity<User> createUser(
@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
return ResponseEntity.ok(user);
}
The controller focuses on:
HTTP Request
↓
Validation
↓
Service
↓
HTTP Response
Exception handling happens separately.
This makes the application easier to read and maintain.
8. My Basic Exception Structure
For a typical Spring Boot application, I like to keep the structure simple:
exception
├── BusinessException
├── ResourceNotFoundException
├── ResourceAlreadyExistsException
└── GlobalExceptionHandler
As the application grows, additional exception types can be added when they represent meaningful business concepts.
The important thing is not to create dozens of exception classes unnecessarily.
Start with a small set:
Not Found
Conflict
Business Error
Validation Error
Unexpected Error
That is often enough for many applications.
A Simple Mental Model
I think about exceptions in three categories:
Business Errors
↓
Expected application behavior
Validation Errors
↓
Invalid client input
System Errors
↓
Unexpected failures
Each category should have a predictable API response.
For example:
| Error Type | HTTP Status |
|---|---|
| Resource not found | 404 |
| Invalid request | 400 |
| Resource already exists | 409 |
| Unauthorized | 401 |
| Forbidden | 403 |
| Unexpected error | 500 |
The exact structure may vary depending on the application, but consistency is important.
Final Thoughts
Global exception handling is not one of the most exciting parts of a Spring Boot application.
But it has a big impact on the overall quality of the API.
A good exception handling strategy should:
- Keep controllers clean
- Separate business logic from HTTP concerns
- Return consistent error responses
- Avoid exposing internal implementation details
- Make the application easier to maintain
My preferred starting point is simple:
Custom Exceptions
↓
@RestControllerAdvice
↓
Consistent Error Response
↓
Structured Logging
You do not need a complicated exception framework to get started.
A small and consistent structure is usually enough.
As the application grows, you can evolve the error model based on real requirements.
How do you handle exceptions in your Spring Boot applications?
Do you use:
-
@RestControllerAdvice? - Custom business exceptions?
- A standard API error response?
- Problem Details / RFC 9457?
I'd love to hear how other Java developers structure exception handling in production applications.
I'm building and sharing open-source projects around Java, Spring Boot, enterprise application architecture, and AI-powered applications.
My goal is to explore practical patterns and reusable tools that help developers build enterprise applications faster.
Top comments (0)