Exception Handling in Spring Boot Application [2025 Edition]
Exception handling is a crucial part of building robust APIs in Spring Boot. In real-world applications, unexpected errors can occur due to invalid input, missing resources, or internal server issues. Proper exception handling helps in:
✅ Returning meaningful error messages to the client.
✅ Improving API reliability and preventing crashes.
✅ Enhancing debugging and logging.
Introduction
In this tutorial, I’ll guide you through different approaches for handling error responses based on your Java version:
✔️ Java 19 or higher — Use the built-in ProblemDetail for a standardized approach.
✔️ Java 14 — Continue using ErrorResponse as a Java record.
✔️ Java 13 or earlier — Manually create a POJO ErrorResponse.
Let’s get started! 🚀
For non-members, read this article for free on my blog: Exception Handling in Spring Boot Application [2025 Edition].
🔹 Reference to the Previous Tutorial
In our previous tutorial, we built a RESTful API for User Management using Spring Boot, MySQL, and Spring Data JPA. Now, we will extend that tutorial by implementing exception handling in the same application.
Prerequisite tutorial: Building RESTful Web Services Using Spring Boot, Spring Data JPA, and MySQL
🚀 Step 1: Create a Custom Error Response Class
📌 We need a standard structure to return error messages when exceptions occur.
📌 Create ErrorResponse.java
inside net.javaguides.usermanagement.exception
package net.javaguides.usermanagement.exception;
import java.time.LocalDateTime;
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();
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
}
📌 Explanation
✔️ message → Stores the error message.
✔️ status → Stores the HTTP status code (e.g., 404, 500).
✔️ timestamp → Stores the time the error occurred.
✔️ Constructor initializes timestamp
automatically.
Using Java Record for ErrorResponse
If you are using Java 14+, then use Java Record. We will refactor the ErrorResponse
class to use Java Record, eliminating the need for boilerplate code while making it immutable.
Create ErrorResponse.java
inside net.javaguides.usermanagement.exception
package net.javaguides.usermanagement.exception;
import java.time.LocalDateTime;
public record ErrorResponse(String message, int status, LocalDateTime timestamp) {
public ErrorResponse(String message, int status) {
this(message, status, LocalDateTime.now());
}
}
📌 Explanation
✔️ record
→ Automatically generates constructors, getters, equals()
, hashCode()
, and toString()
.
✔️ message
→ Stores the error message.
✔️ status
→ Stores the HTTP status code (e.g., 404
, 500
).
✔️ timestamp
→ Stores the time the error occurred (auto-initialized).
✔️ Simplified Constructor → If only message
and status
are provided, timestamp
is automatically set.
🚀 Step 2: Create a Global Exception Handler
Instead of handling exceptions inside each controller, we will use @RestControllerAdvice
to centralize exception handling.
📌 Create GlobalExceptionHandler.java
inside net.javaguides.usermanagement.exception
package net.javaguides.usermanagement.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
// Handle ResourceNotFoundException
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
ErrorResponse errorResponse = new ErrorResponse(
ex.getMessage(),
HttpStatus.NOT_FOUND.value()
);
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
// Handle Generic Exceptions
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse(
"An unexpected error occurred: " + ex.getMessage(),
HttpStatus.INTERNAL_SERVER_ERROR.value()
);
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
📌 Explanation of GlobalExceptionHandler.java
✔️ @RestControllerAdvice
→ Makes this class a global exception handler for all controllers.
✔️ @ExceptionHandler(ResourceNotFoundException.class)
→ Handles cases where a user is not found in the database.
✔️ @ExceptionHandler(Exception.class)
→ Catches all other unhandled exceptions and returns a 500 (Internal Server Error).
=> Using Java Record in GlobalExceptionHandler.java
For using ErrorResponse record, modify GlobalExceptionHandler.java
inside net.javaguides.usermanagement.exception
package net.javaguides.usermanagement.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
// Handle ResourceNotFoundException
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
return new ResponseEntity<>(
new ErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND.value()),
HttpStatus.NOT_FOUND
);
}
// Handle Generic Exceptions
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
return new ResponseEntity<>(
new ErrorResponse("An unexpected error occurred: " + ex.getMessage(),
HttpStatus.INTERNAL_SERVER_ERROR.value()),
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
✔️ Uses ErrorResponse record → Eliminates boilerplate, making the response immutable and thread-safe.
🚀 Step 3: Create ResourceNotFoundException
📌 Create ResourceNotFoundException.java
inside net.javaguides.usermanagement.exception
package net.javaguides.usermanagement.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
🚀 Step 4: Modify the Service Layer to Use ResourceNotFoundException
📌 Modify UserServiceImpl.java
inside net.javaguides.usermanagement.service.impl
@Override
public UserDto getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
return userMapper.toDto(user);
}
@Override
public UserDto updateUser(Long id, UserDto userDto) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
user.setFirstName(userDto.firstName());
user.setLastName(userDto.lastName());
user.setEmail(userDto.email());
user.setDateOfBirth(userDto.dateOfBirth());
User updatedUser = userRepository.save(user);
return userMapper.toDto(updatedUser);
}
@Override
public void deleteUser(Long id) {
if (!userRepository.existsById(id)) {
throw new ResourceNotFoundException("User not found with id: " + id);
}
userRepository.deleteById(id);
}
📌 Explanation
✔️ Uses orElseThrow()
to throw ResourceNotFoundException
if a user does not exist.
✔️ Helps return 404 Not Found instead of null values.
🚀 Step 5: Testing Exception Handling Using Postman
✅ 1. Testing ResourceNotFoundException
❌ Get a Non-Existent User (GET Request)
📌 GET Request URL:
http://localhost:8080/api/users/100
✅ 2. Testing Generic Exception Handling
❌ Invalid URL (POST Request)
📌 POST Request URL:
Valid URL: http://localhost:8080/api/users
Invalid URL: http://localhost:8080/api/users/1
🚀 Step 6: Logging Exceptions (Optional, Recommended)
To improve debugging, we can log errors when exceptions occur.
📌 Modify GlobalExceptionHandler.java
to include logging
package net.javaguides.usermanagement.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
logger.error("Resource Not Found: " + ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND.value());
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
logger.error("Internal Server Error: " + ex.getMessage(), ex);
ErrorResponse errorResponse = new ErrorResponse("An unexpected error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR.value());
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
📌 Explanation
✔️ Uses SLF4J Logger to log errors for better debugging.
✔️ Helps track errors in production environments.
Using ProblemDetail
Instead of a Custom ErrorResponse
(Java 19+)
Starting from Java 19, the ProblemDetail
class was introduced as part of Spring 6 / Spring Boot 3 to standardize error responses following the RFC 7807 (Problem Details for HTTP APIs). Instead of using a custom ErrorResponse
class, we can use ProblemDetail
for a more standardized and built-in approach.
🚀 Updated Approach Using ProblemDetail
(Java 19+)
Step 1: Modify the Global Exception Handler
📌 Modify GlobalExceptionHandler.java
inside net.javaguides.usermanagement.exception
package net.javaguides.usermanagement.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.Instant;
@RestControllerAdvice
public class GlobalExceptionHandler {
// Handle ResourceNotFoundException
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleResourceNotFoundException(ResourceNotFoundException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
}
// Handle Generic Exceptions
@ExceptionHandler(Exception.class)
public ProblemDetail handleGenericException(Exception ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR,
"An unexpected error occurred: " + ex.getMessage());
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
}
}
🚀 Step 2: Testing the API with ProblemDetail
✅ 1. Testing ResourceNotFoundException
📌 GET Request URL:
GET http://localhost:8080/api/users/999
📌 Response (ProblemDetail
JSON format):
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "User not found with id: 999",
"instance": "/api/users/999",
"timestamp": "2024-02-05T12:40:56.789Z"
}
✅ 2. Testing Generic Exception Handling 📌 GET Request URL:
GET http://localhost:8080/api/users/abc
📌 Response (ProblemDetail
JSON format):
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred: Failed to convert value of type 'java.lang.String' to required type 'Long'",
"timestamp": "2024-02-05T12:41:10.123Z"
}
🎯 Summary: What We Achieved
✔️ Implemented exception handling in Spring Boot.
✔️ Created a GlobalExceptionHandler
for centralized error handling.
✔️ Handled ResourceNotFoundException
for missing users.
✔️ Tested exception handling using Postman.
✔️ Added logging for better debugging.
Final Recommendation
✔️ Java 19 or higher — Use the built-in ProblemDetail for a standardized approach.
✔️ Java 14 — Continue using ErrorResponse as a Java record.
✔️ Java 13 or earlier — Manually create a POJO ErrorResponse.
🎉 Congratulations! You now have a fully functional, production-ready Spring Boot API with proper exception handling! 🚀🔥