Exception Handling in Spring Boot Application [2025 Edition]

Ramesh Fadatare
6 min readFeb 2, 2025

--

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! 🚀🔥

--

--

Responses (2)