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

This article explains how to replace repetitive try‑catch blocks in Java backend development by using Spring's @ControllerAdvice for centralized exception handling, leveraging Assert assertions and custom enum‑based error codes to create clean, maintainable code and consistent API responses.

Architect's Tech Stack
Architect's Tech Stack
Architect's Tech Stack
Master Unified Exception Handling in Spring: Clean Code with Assertions and Enums

What Is Unified Exception Handling

Since Spring 3.2, the @ControllerAdvice annotation combined with @ExceptionHandler enables developers to define global exception handlers that apply to all controllers, eliminating the need for duplicated try‑catch blocks in each controller or service.

Goal

The aim is to remove more than 95% of try‑catch statements by using Assert (assertions) for business validation, focusing on business logic while the framework handles error propagation.

Implementation

Replace try‑catch with Assert

Using Assert.notNull makes null‑checks concise and readable compared with explicit

if (obj == null) { throw new IllegalArgumentException(...); }

statements.

@Test
public void test1() {
    User user = userDao.selectById(userId);
    Assert.notNull(user, "用户不存在.");
    // ...
}

public void test2() {
    User user = userDao.selectById(userId);
    if (user == null) {
        throw new IllegalArgumentException("用户不存在.");
    }
    // ...
}

Assert Source Code

public abstract class Assert {
    public static void notNull(@Nullable Object object, String message) {
        if (object == null) {
            throw new IllegalArgumentException(message);
        }
    }
    // other overloads omitted for brevity
}

Custom Exception Enum

Define an enum that implements BusinessExceptionAssert and holds an error code and message. Each enum constant represents a specific business error.

public interface IResponseEnum {
    int getCode();
    String getMessage();
}

public interface BusinessExceptionAssert extends IResponseEnum, Assert {
    default BaseException newException(Object... args) {
        String msg = MessageFormat.format(this.getMessage(), args);
        return new BusinessException(this, args, msg);
    }
    default BaseException newException(Throwable t, Object... args) {
        String msg = MessageFormat.format(this.getMessage(), args);
        return new BusinessException(this, args, msg, t);
    }
}

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;
    // getters omitted
}

Unified Exception Handler

A class annotated with @ControllerAdvice defines methods for handling business exceptions, servlet‑related exceptions, binding errors, validation errors, and generic exceptions. Each method logs the error and returns a consistent ErrorResponse containing a code and message.

@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
public class UnifiedExceptionHandler {
    // environment flag
    private static final String ENV_PROD = "prod";
    @Autowired
    private UnifiedMessageSource unifiedMessageSource;
    @Value("${spring.profiles.active}")
    private String profile;

    private 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);
            return new ErrorResponse(code, getMessage(be));
        }
        return new ErrorResponse(code, e.getMessage());
    }

    @ExceptionHandler(BindException.class)
    @ResponseBody
    public ErrorResponse handleBindException(BindException e) {
        log.error("参数绑定校验异常", e);
        return wrapperBindingResult(e.getBindingResult());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
        log.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);
            return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), getMessage(be));
        }
        return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
    }
}

Making 404 Throw an Exception

Set spring.mvc.throw-exception-if-no-handler-found=true and spring.resources.add-mappings=false in application.properties so that a missing URL results in NoHandlerFoundException, which is handled by handleServletException.

Unified Response Structure

Define a base response class with code and message. CommonResponse adds a data field, while QueryDataResponse includes pagination fields. Shortcut classes R<T> and QR<T> simplify response creation.

Testing the Handlers

Various HTTP requests (GET, POST, invalid URLs, missing parameters, validation failures) demonstrate how custom enums like ResponseEnum.LICENCE_NOT_FOUND and ResponseEnum.BAD_LICENCE_TYPE produce consistent error codes and messages, how 404 is captured, and how unknown database errors are wrapped into a generic server‑error response.

Conclusion

By combining assertions, enum‑based error codes, and a centralized @ControllerAdvice handler, Java backend projects can dramatically reduce boilerplate try‑catch code, improve readability, and provide uniform API error responses across all layers.

JavaSpringEnumsAssertionsUnified response
Architect's Tech Stack
Written by

Architect's Tech Stack

Java backend, microservices, distributed systems, containerized programming, and more.

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.