How to Build a Clean Unified Exception Handling System in Spring Boot
This article walks through the problem of scattered try‑catch blocks in Java back‑ends, explains the use of Spring's @ControllerAdvice together with a custom Assert‑based validation and error‑code enum, and demonstrates a complete unified exception handling solution with production‑ready response formatting and extensive code examples.
Background
In Java backend development, handling exceptions with repetitive try { … } catch { … } finally { … } blocks reduces readability and creates a lot of boilerplate code. The article shows how to replace these scattered blocks with a unified exception handling mechanism that keeps business logic clean and concise.
What Is Unified Exception Handling
Spring 3.2 introduced @ControllerAdvice, which can be combined with @ExceptionHandler, @InitBinder, and @ModelAttribute to apply cross‑cutting concerns to all controllers. Only @ExceptionHandler directly deals with exception processing, allowing a single class to define handling logic for any controller.
Assert‑Based Validation
Instead of writing explicit
if (obj == null) { throw new IllegalArgumentException(...); }checks, the author uses Spring's org.springframework.util.Assert and creates a custom Assert interface that throws domain‑specific BaseException. Test methods illustrate the readability gain:
public void test1() {
User user = userDao.selectById(userId);
Assert.notNull(user, "User does not exist.");
}
public void test2() {
User user = userDao.selectById(userId);
if (user == null) {
throw new IllegalArgumentException("User does not exist.");
}
}The custom Assert interface defines default methods assertNotNull(Object obj) and assertNotNull(Object obj, Object... args) that delegate to newException methods, enabling the exception type and message to be derived from an enum.
/**
* Create an exception instance.
* @param args arguments for message formatting
* @return a new BaseException
*/
BaseException newException(Object... args);
/**
* Create an exception instance with a cause.
* @param t the throwable cause
* @param args arguments for message formatting
* @return a new BaseException
*/
BaseException newException(Throwable t, Object... args);
/**
* Assert that the given object is not null; otherwise throw the exception defined by the enum.
* @param obj the object to check
*/
default void assertNotNull(Object obj) {
if (obj == null) {
throw newException(obj);
}
}
/**
* Assert that the given object is not null; otherwise throw the exception with a formatted message.
* @param obj the object to check
* @param args arguments for the message template
*/
default void assertNotNull(Object obj, Object... args) {
if (obj == null) {
throw newException(args);
}
}Enum for Error Codes
A response enum implements BusinessExceptionAssert and provides a pair of code and message. Adding a new error scenario only requires a new enum constant, not a new exception class.
/**
* Business exception interface extending the response enum and custom Assert.
*/
public interface BusinessExceptionAssert extends IResponseEnum, Assert {
@Override
default BaseException newException(Object... args) {
String msg = MessageFormat.format(this.getMessage(), args);
return new BusinessException(this, args, msg);
}
@Override
default BaseException newException(Throwable t, Object... args) {
String msg = MessageFormat.format(this.getMessage(), args);
return new BusinessException(this, args, msg, t);
}
}
/**
* Enum defining error codes and messages for business validation.
*/
@Getter
@AllArgsConstructor
public enum ResponseEnum implements BusinessExceptionAssert {
BAD_LICENCE_TYPE(7001, "Bad licence type."),
LICENCE_NOT_FOUND(7002, "Licence not found.");
private final int code;
private final String message;
}Unified Exception Handler
The @ControllerAdvice class defines handlers for business exceptions, generic BaseException, servlet‑level exceptions, binding errors, validation errors, and unknown exceptions. It also distinguishes production and development environments to hide technical details from end users.
@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
@ConditionalOnMissingBean(UnifiedExceptionHandler.class)
public class UnifiedExceptionHandler {
private static final String ENV_PROD = "prod";
@Autowired
private UnifiedMessageSource unifiedMessageSource;
@Value("${spring.profiles.active}")
private String profile;
/**
* Retrieve an internationalized message for the given BaseException.
*/
public String getMessage(BaseException e) {
String code = "response." + e.getResponseEnum().toString();
String message = unifiedMessageSource.getMessage(code, e.getArgs());
if (message == null || message.isEmpty()) {
return e.getMessage();
}
return message;
}
/**
* Handle custom business exceptions.
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
public ErrorResponse handleBusinessException(BaseException e) {
log.error(e.getMessage(), e);
return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
}
/**
* Handle generic BaseException (non‑business custom exceptions).
*/
@ExceptionHandler(BaseException.class)
@ResponseBody
public ErrorResponse handleBaseException(BaseException e) {
log.error(e.getMessage(), e);
return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
}
/**
* Handle servlet‑level exceptions such as 404, method not supported, etc.
*/
@ExceptionHandler({
NoHandlerFoundException.class,
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
HttpMediaTypeNotAcceptableException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
MissingServletRequestPartException.class,
AsyncRequestTimeoutException.class
})
@ResponseBody
public ErrorResponse handleServletException(Exception e) {
log.error(e.getMessage(), e);
int code = CommonResponseEnum.SERVER_ERROR.getCode();
try {
ServletResponseEnum servletEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName());
code = servletEnum.getCode();
} catch (IllegalArgumentException ex) {
log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName());
}
if (ENV_PROD.equals(profile)) {
BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
String message = getMessage(baseException);
return new ErrorResponse(code, message);
}
return new ErrorResponse(code, e.getMessage());
}
/**
* Handle parameter binding validation errors.
*/
@ExceptionHandler(BindException.class)
@ResponseBody
public ErrorResponse handleBindException(BindException e) {
log.error("Parameter binding validation error", e);
return wrapperBindingResult(e.getBindingResult());
}
/**
* Handle method argument validation errors.
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
log.error("Parameter validation error", e);
return wrapperBindingResult(e.getBindingResult());
}
/**
* Convert binding results into a unified error response.
*/
private ErrorResponse wrapperBindingResult(BindingResult bindingResult) {
StringBuilder msg = new StringBuilder();
for (ObjectError error : bindingResult.getAllErrors()) {
msg.append(", ");
if (error instanceof FieldError) {
msg.append(((FieldError) error).getField()).append(": ");
}
msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage());
}
return new ErrorResponse(ArgumentResponseEnum.VALID_ERROR.getCode(), msg.substring(2));
}
/**
* Catch‑all handler for unexpected exceptions.
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public ErrorResponse handleException(Exception e) {
log.error(e.getMessage(), e);
if (ENV_PROD.equals(profile)) {
int code = CommonResponseEnum.SERVER_ERROR.getCode();
BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
String message = getMessage(baseException);
return new ErrorResponse(code, message);
}
return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
}
}Exception Classification
The handler groups exceptions into two major categories: controller‑level HTTP errors (e.g., 404, method not supported, media‑type mismatches) and service‑level business errors defined by the enum. The article lists each exception type and the corresponding response code.
Making 404 Throw an Exception
By adding the following properties, missing URLs trigger NoHandlerFoundException instead of the default Whitelabel page, allowing the unified handler to process them.
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=falseUnified Response Structure
All API responses share a base class containing code and message. Successful payloads use CommonResponse, and paginated results use QueryDataResponse. The author introduces shorthand wrappers R and QR for concise return statements, e.g., return new R<>(data); or return new QR<>(queryData);.
Verification
The article walks through a sample project that uses MyBatis‑Plus. It demonstrates how the handler captures:
Custom business errors such as Licence not found and Bad licence type.
Controller‑level errors like 404 (missing URL) and HTTP method not supported.
Parameter binding and validation errors, showing the aggregated error message.
Unexpected database errors when the entity class and table schema are mismatched.
Screenshots (omitted here) illustrate each scenario.
Conclusion
By combining Assert‑based validation, an error‑code enum, and a @ControllerAdvice handler, developers obtain a clean, maintainable way to manage most exceptions in a Spring backend. The solution scales across services and can be packaged as a reusable common module for future projects.
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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
