Master Unified Exception Handling in Spring Boot: Clean Code with Assertions and Enums

This article explains how to replace repetitive try‑catch blocks in Java Spring applications with a unified exception handling strategy using @ControllerAdvice, custom Assert utilities, and enum‑based error codes, providing clean, maintainable code and consistent API responses.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Master Unified Exception Handling in Spring Boot: Clean Code with Assertions and Enums

Background

In Java development, handling exceptions often leads to repetitive try { ... } catch { ... } finally { ... } blocks that clutter code and reduce readability. The article compares a messy try‑catch style with a clean controller implementation.

Elegant controller code eliminates redundant exception handling.

What Is Unified Exception Handling

Spring 3.2 introduced @ControllerAdvice which can be combined with @ExceptionHandler, @InitBinder, and @ModelAttribute. The @ExceptionHandler annotation defines a method that handles specific exceptions thrown from any controller, allowing centralized processing.

Goal

Eliminate over 95% of try catch blocks by using Assert statements to validate business conditions, focusing on business logic rather than boilerplate error handling.

Unified Exception Handling in Practice

Using Assert Instead of Throwing Exceptions

Spring’s org.springframework.util.Assert provides methods like Assert.notNull(object, "message") which internally throws an IllegalArgumentException if the condition fails.

@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.");
    }
}

Custom Assert interfaces can create domain‑specific exceptions.

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);
        }
    }
}

Enum‑Based Error Codes

Define an enum implementing BusinessExceptionAssert that supplies error codes and messages.

public enum ResponseEnum implements BusinessExceptionAssert {
    BAD_LICENCE_TYPE(7001, "Bad licence type."),
    LICENCE_NOT_FOUND(7002, "Licence not found.");
    private int code;
    private String message;
    // getters omitted
}

Usage in service layer:

private void checkNotNull(Licence licence) {
    ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
}

Unified Exception Handler Class

@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
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();
        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 ex) {
            log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName());
        }
        if (ENV_PROD.equals(profile)) {
            code = CommonResponseEnum.SERVER_ERROR.getCode();
            BaseException be = new BaseException(CommonResponseEnum.SERVER_ERROR);
            return new ErrorResponse(code, getMessage(be));
        }
        return new ErrorResponse(code, e.getMessage());
    }

    @ExceptionHandler(BindException.class)
    @ResponseBody
    public ErrorResponse handleBindException(BindException e) {
        log.error("Parameter binding exception", e);
        return wrapperBindingResult(e.getBindingResult());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
        log.error("Parameter validation exception", 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)) {
            int code = CommonResponseEnum.SERVER_ERROR.getCode();
            BaseException be = new BaseException(CommonResponseEnum.SERVER_ERROR);
            return new ErrorResponse(code, getMessage(be));
        }
        return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
    }
}

Handling 404 as an Exception

By default Spring forwards 404 to /error. To make it throw an exception, add to application.properties:

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

Now NoHandlerFoundException is caught by handleServletException and a unified error response is returned.

Unified Response Structure

All API responses contain code and message. Successful responses also include data. Base classes:

class BaseResponse { int code; String message; }
class CommonResponse<T> extends BaseResponse { T data; }
class ErrorResponse extends BaseResponse { }
class QueryDataResponse<T> extends CommonResponse<QueryData<T>> { }

Convenient wrappers R and QR simplify usage: new R<>(data), new QR<>(queryData).

Validation and Testing

The article demonstrates how custom assertions, enum‑based errors, and the unified handler work in practice: missing resources, invalid licence types, parameter binding errors, and unexpected database errors are all captured and returned with consistent code / message payloads.

Conclusion

Combining assertions with enum‑driven error codes and a centralized @ControllerAdvice handler greatly reduces boilerplate, improves readability, and provides a uniform error‑response format. For production environments, the handler can hide internal details and return generic messages, while still supporting internationalization via UnifiedMessageSource.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentException HandlingEnumSpring BootAssertUnified Error Response
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.