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.

Java Backend Technology
Java Backend Technology
Java Backend Technology
Mastering Unified Exception Handling in Spring: Clean Code with Assertions and Enum‑Based Errors

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.

Ugly try‑catch code
Ugly try‑catch code
Elegant controller code
Elegant controller code

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=false

Summary

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.

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.

BackendJavaException HandlingspringenumAssertControllerAdvice
Java Backend Technology
Written by

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!

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.