Master Java Exception Handling in Spring MVC: Custom Exceptions & Global Advice
This article explains how to design and implement robust Java exception handling for business systems using Spring MVC, covering when to create custom exceptions, how to structure service and controller code, and how to centralize error responses with @ControllerAdvice for clean, maintainable backend development.
Why Proper Exception Handling Matters
In large‑scale business systems the majority of bugs stem from unclear error handling, tightly coupled service‑controller code, and missing stack traces, which make debugging and maintenance difficult. Using Spring’s exception mechanism can separate business failures from programming errors and provide clear feedback to callers.
When to Define Custom Exceptions
Throwing a generic AppException everywhere wastes log space and hides the real failure point.
Having only one exception type prevents precise categorisation of error conditions.
Future changes to the exception class become painful because callers must be updated.
Implementing a Business Exception
public class ServiceException extends RuntimeException {
/** Business processing failure */
public ServiceException(String reason) {
super(reason);
}
}Controller Layer Example
@PutMapping("{userID}")
public JSONResult updateUser(@PathVariable("userID") Integer userID,
@RequestBody UpdateUserForm userForm) {
User user = new User();
BeanUtils.copyProperties(userForm, user);
user.setUserId(userID);
userService.updateUser(user);
JSONResult json = new JSONResult();
json.put("user", user);
return json;
}Service Layer Validation and Throwing Exceptions
public void updateUser(User user) {
User userOrig = userDao.getUserById(user.getUserID());
if (userOrig == null) {
throw new ServiceException("User does not exist");
}
if (userOrig.isLocked()) {
throw new ServiceException("User is locked, cannot modify");
}
if (!user.getVersion().equals(userOrig.getVersion())) {
throw new ServiceException("User has been modified by another transaction");
}
// TODO: persist user data
}Global Exception Handling with @ControllerAdvice
@ControllerAdvice(basePackages = {"com.xxx.xxx.bussiness.xxx"})
public class ModuleControllerAdvice {
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleControllerAdvice.class);
private static final Logger SERVICE_LOGGER = LoggerFactory.getLogger(ServiceException.class);
@ExceptionHandler(ServiceException.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
private JSONResult handleServiceException(ServiceException exception) {
String message = "Business processing failed, reason: " + exception.getLocalizedMessage();
SERVICE_LOGGER.info(message);
JSONResult json = new JSONResult();
json.setCode(500001); // 500000 system error, 500001 business error
json.setMessage(message);
return json;
}
}Classification of Exceptions
Logical exceptions – represent business rule violations that the user caused.
Code errors – programming bugs such as NullPointerException or IllegalArgumentException.
Domain‑specific exceptions – special cases that cannot be anticipated in generic code.
Key Takeaways
Separate business exceptions from system errors and log them independently.
Prefer unchecked (RuntimeException) for business failures to avoid changing method signatures.
Use a single exception class that carries a clear reason message and handle it centrally with @ControllerAdvice.
Never use try‑catch for normal business flow; let exceptions interrupt execution and let the framework manage the response.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
