Mistakes happen. In every application. No matter how well it has been developed. But how you deal with errors determines whether the application remains stable and secure – or turns into a nightmare. In this article, we look at how to implement exception handling in Spring Boot correctly to improve the user experience, facilitate debugging and, above all, minimize security risks.

Why is exception handling in Spring Boot so important?
Spring Boot offers some mechanisms for exception handling out of the box, but these are often not sufficient. Missing or poor exception handling leads to:
- Security vulnerabilities (e.g. by leaking stack traces or sensitive data)
- Poor user experience (e.g. due to cryptic error messages)
- Maintenance problems (because it is not clear where the error is coming from)
So: We need a clear strategy to intercept exceptions cleanly, log them and process them appropriately. Let’s go through this step by step.

Differences between checked and unchecked exceptions
In Spring Boot, a distinction is made between checked exceptions and unchecked exceptions in order to handle errors in the application:
- Checked exceptions are derived from Exception (but not from RuntimeException) and must be explicitly handled or passed on. They are suitable for expected error cases that should be handled by the caller, e.g. missing files or connection problems (IOException, SQLException).
- Unchecked exceptions are derived from RuntimeException and do not have to be handled explicitly. They are often used for programming errors, e.g. null references or invalid states (NullPointerException, IllegalArgumentException).
In Spring Boot, unchecked exceptions are preferred because they improve the readability of the code by not necessarily having to be handled with try-catch. Instead, Spring Boot can provide global exception handling mechanisms with @ControllerAdvice and @ExceptionHandler. For example, centralized error handling can convert all ResourceNotFoundException errors into an HTTP 404 response.
1. Unchecked Exception Example (Recommended in Spring Boot)
Let’s assume we have a service that searches for a resource using an ID. If the resource does not exist, we throw an unchecked exception (RuntimeException).
Exception class (Unchecked)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Service class
@Service
public class UserService {
private final Map<Long, String> users = Map.of(1L, "Alice", 2L, "Bob");
public String getUserById(Long id) {
return users.getOrDefault(id, null);
}
}
Controller
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<String> getUser(@PathVariable Long id) {
String user = userService.getUserById(id);
if (user == null) {
throw new ResourceNotFoundException("User with ID " + id + " not found");
}
return ResponseEntity.ok(user);
}
}
Globale Exception-Handling class
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleResourceNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
Result: If the user with the specified ID does not exist, the API returns an HTTP 404 – Not Found response with a clear error message.
2. Checked Exception Example
Let’s assume we are accessing an external database and need to handle a possible SQLException.
Service class with Checked Exception
@Service
public class DatabaseService {
public String fetchData() throws SQLException {
throw new SQLException("Database connection failed!");
}
}
Controller with Checked Exception
@RestController
@RequestMapping("/data")
public class DataController {
private final DatabaseService databaseService;
public DataController(DatabaseService databaseService) {
this.databaseService = databaseService;
}
@GetMapping
public ResponseEntity<String> getData() {
try {
return ResponseEntity.ok(databaseService.fetchData());
} catch (SQLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error fetching data: " + e.getMessage());
}
}
}
Problem: The controller must use try-catch to handle the SQLException, which makes the code more confusing.

The basics: Catching exceptions in Spring Boot
Spring Boot comes with a global exception handler that handles errors by default by returning a generic error page or a JSON response. This is sufficient for simple applications, but more control is needed in practice.
Global error handling with @ControllerAdvice
With @ControllerAdvice we can intercept all exceptions of our application centrally and provide a uniform response. Example:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleResourceNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error has occurred");
}
}
Here we specifically catch ResourceNotFoundException and return a 404 status code. All other errors end up in a generic handler.
Specific error pages for web applications
A user-friendly error page can be defined for classic Spring MVC web applications:
- Create an error.html file under src/main/resources/templates.
- Spring Boot automatically displays this page when errors occur.
If you want different error pages for different status codes, you can store HTML files such as 404.html or 500.html under src/main/resources/templates/error/.

Advanced exception handling with @ResponseStatus and ErrorResponse
If you want to link exceptions with HTTP status codes, you can use @ResponseStatus:
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Or even better: A consistent error format with its own ErrorResponse class.
Tip: If you want to enable exact tracing of an error, you can add your own unique error codes. This allows you to determine the exact origin. The difficulty here is to set the error codes uniquely. To do this, you can use a central enum and expand it when making adjustments. I would recommend starting with a 6-8 digit number. Something like “1000000”. Depending on your logic, the first number can stand for a different microservice or controller.
public class ErrorResponse {
private String message;
private int status;
private LocalDateTime timestamp;
public ErrorResponse(String message, int status) {
this.message = message;
this.status = status;
this.timestamp = LocalDateTime.now();
}
// Getter & Setter
}
And in the @ControllerAdvice:
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND.value());
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
This will give you a clean JSON response in the event of errors:
{
"message": "Resource not found",
"status": 404,
"timestamp": "2025-02-18T10:15:30"
}

Custom exception classes with additional fields
Error responses with more context are often required. A separate exception with additional fields can help:
public class BusinessException extends RuntimeException {
private final int errorCode;
public BusinessException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
public int getErrorCode() {
return errorCode;
}
}
And in the exception handler:
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), ex.getErrorCode());
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

Handling multiple @ControllerAdvice files
In larger applications, it can be useful to use several @ControllerAdvice classes. For example, for different modules:
@RestControllerAdvice(basePackages = "com.example.user")
public class UserExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found");
}
}
@RestControllerAdvice(basePackages = "com.example.order")
public class OrderExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<String> handleOrderNotFound(OrderNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Order not found");
}
}
Set priority of the @ControllerAdvice classes
Spring Boot processes @ControllerAdvice classes in a specific order. If there are several, the priority can be defined with @Order:
@RestControllerAdvice
@Order(1)
public class HighPriorityExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleRuntime(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("High priority Error handling");
}
}
@RestControllerAdvice
@Order(2)
public class DefaultExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneric(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Standard error handling");
}
}
@Order(1) ensures that this exception handler is executed before others.

Security aspects of exception handling
Poorly implemented exception handling can pose a security risk. Here are some common mistakes and how to avoid them:
Leaking stack traces in API responses
- Never simply write exceptions to the response with ex.printStackTrace()!
- Use loggers instead:
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException(Exception ex) {
logger.error("Internal error: ", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error has occurred");
}

Performance optimization for exception handling
Exceptions in Java are expensive because they generate stack traces. Here are best practices to improve performance:
1. do not use exceptions for control flow
- Bad practice: use try-catch for logic.
- Better: Check beforehand with if or optional.
2. unchecked exceptions for business logic
- Only use checked exceptions for external API calls or I/O operations.
3. log stack traces only for real errors
- Exception: A concise logging message is sufficient for expected errors.

Handling of validation errors with @Valid and @ExceptionHandler
Spring Boot supports automatic validation with @Valid:
public class User {
@NotNull
private String name;
@Email
private String email;
// Getter & Setter
}
In the controller:
@PostMapping("/users")
public ResponseEntity<String> createUser(@Valid @RequestBody User user) {
return ResponseEntity.ok("User saved");
}
But what if the validation fails? This is where @ExceptionHandler comes into play:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
This gives us a structured error message:
{
"name": "Name must not be empty",
"email": "Must be a valid e-mail"
}
Conclusion
Exception handling in Spring Boot is not only a question of cleanliness, but also of security and performance. With these techniques, you will have stable, secure and professional exception handling in your Spring Boot application. 🚀