Mastering Unified Exception Handling in Spring: Clean Code with Assertions and Enum‑Based Errors
This article explains how to replace repetitive try‑catch blocks in Java Spring applications with a unified exception handling strategy that leverages @ControllerAdvice, custom Assert utilities, and enum‑driven error codes to produce cleaner, more maintainable backend code.
Background: In software development handling exceptions inevitably leads to many try { ... } catch { ... } finally { ... } blocks, which create redundant code and hurt readability.
Below are two illustrations of typical code styles.
What Is Unified Exception Handling
Since Spring 3.2 the @ControllerAdvice annotation can be combined with @ExceptionHandler, @InitBinder, and @ModelAttribute to apply the same exception‑handling logic to all controllers, eliminating the need to repeat code in each controller.
Goal
Eliminate more than 95% of try‑catch blocks by using Assert (assertion) statements to validate business conditions, allowing developers to focus on core logic while the framework handles error reporting.
Practical Unified Exception Handling
Replace throw with Assert
Spring provides org.springframework.util.Assert for concise validation. Example test code:
@Test
public void test1() {
...
User user = userDao.selectById(userId);
Assert.notNull(user, "User does not exist.");
...
}
@Test
public void test2() {
User user = userDao.selectById(userId);
if (user == null) {
throw new IllegalArgumentException("User does not exist.");
}
}We can create our own Assert interface that throws custom BaseException instances:
public interface Assert {
BaseException newException(Object... args);
BaseException newException(Throwable t, Object... args);
default void assertNotNull(Object obj) {
if (obj == null) {
throw newException(obj);
}
}
default void assertNotNull(Object obj, Object... args) {
if (obj == null) {
throw newException(args);
}
}
}Combine the assertion with an enum that carries an error code and message:
public interface IResponseEnum {
int getCode();
String getMessage();
}
public class BusinessException extends BaseException {
public BusinessException(IResponseEnum responseEnum, Object[] args, String message) {
super(responseEnum, args, message);
}
public BusinessException(IResponseEnum responseEnum, Object[] args, String message, Throwable cause) {
super(responseEnum, args, message, cause);
}
}
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);
}
}
@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;
}Usage in service code becomes very clean:
private void checkNotNull(Licence licence) {
ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
}Define Unified Exception Handler Class
@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;
public String getMessage(BaseException e) {
String code = "response." + e.getResponseEnum().toString();
String message = unifiedMessageSource.getMessage(code, e.getArgs());
return (message == null || message.isEmpty()) ? e.getMessage() : message;
}
@ExceptionHandler(BusinessException.class)
@ResponseBody
public ErrorResponse handleBusinessException(BaseException e) {
log.error(e.getMessage(), e);
return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
}
@ExceptionHandler(BaseException.class)
@ResponseBody
public ErrorResponse handleBaseException(BaseException e) {
log.error(e.getMessage(), e);
return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
}
@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 ignored) {
log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName());
}
if (ENV_PROD.equals(profile)) {
BaseException be = new BaseException(CommonResponseEnum.SERVER_ERROR);
String msg = getMessage(be);
return new ErrorResponse(code, msg);
}
return new ErrorResponse(code, e.getMessage());
}
@ExceptionHandler(BindException.class)
@ResponseBody
public ErrorResponse handleBindException(BindException e) {
log.error("Parameter binding validation error", e);
return wrapperBindingResult(e.getBindingResult());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
log.error("Parameter validation error", e);
return wrapperBindingResult(e.getBindingResult());
}
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));
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ErrorResponse handleException(Exception e) {
log.error(e.getMessage(), e);
if (ENV_PROD.equals(profile)) {
BaseException be = new BaseException(CommonResponseEnum.SERVER_ERROR);
String msg = getMessage(be);
return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), msg);
}
return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
}
}The handler class categorises exceptions into three logical groups: pre‑controller (e.g., 404, method not allowed), business exceptions (custom BusinessException), and unknown exceptions (runtime/database errors). Each group returns a JSON object containing code, message, and optionally data.
Unusual 404 Handling
By default Spring forwards a missing URL to the Whitelabel error page. Adding the following properties forces Spring to throw NoHandlerFoundException so the unified handler can return a structured error response:
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=falseSummary
Combining assertions, enum‑driven error codes, and a global @ControllerAdvice handler dramatically reduces boiler‑plate try‑catch code, improves readability, and provides a consistent error‑response format across the backend. The approach is easily reusable across projects by packaging it as a common library.
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.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
