Master Unified Exception Handling in Spring with @ControllerAdvice and Assert

This article explains how to replace repetitive try‑catch blocks in Spring applications by using @ControllerAdvice for global exception handling, custom Assert utilities, and enum‑based error codes, resulting in cleaner, more maintainable backend code and consistent API responses.

Java Interview Crash Guide
Java Interview Crash Guide
Java Interview Crash Guide
Master Unified Exception Handling in Spring with @ControllerAdvice and Assert

What is Unified Exception Handling

In Java Spring projects, developers often end up writing many try { … } catch { … } finally { … } blocks, which makes the code noisy and hard to read. A unified approach moves exception handling out of individual controllers and services, improving readability and maintainability.

Using @ControllerAdvice

Spring 3.2 introduced @ControllerAdvice, which can be combined with @ExceptionHandler, @InitBinder, and @ModelAttribute. By annotating a class with @ControllerAdvice, you can define methods that handle specific exception types for all controllers.

Replacing try‑catch with Assert

Spring provides org.springframework.util.Assert for concise null checks. The article shows a test example:

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

The Assert.notNull call is more elegant than the explicit if check.

Custom Assert Interface

To throw domain‑specific exceptions, the article defines a custom Assert interface with default methods:

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

Implementations decide which concrete exception to throw.

Enum‑Based Error Codes

Instead of creating many exception classes, an enum can hold code and message pairs. Example:

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

Using the enum, you can write ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence); instead of manual checks.

Unified Exception Handler Class

The core of the solution is a @ControllerAdvice class that handles different exception groups:

@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 servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName());
            code = servletExceptionEnum.getCode();
        } catch (IllegalArgumentException e1) {
            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(CommonResponseEnum.SERVER_ERROR.getCode(), getMessage(be));
        }
        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 binding 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);
            return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), getMessage(be));
        }
        return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
    }
}

The handler groups exceptions into three categories: pre‑controller (e.g., 404, method not allowed), service‑layer custom exceptions, and unknown exceptions.

Unified Response Structure

All API responses share a base class with code and message. Successful responses add a data field, and paginated results use a QueryDataResponse that extends CommonResponse. Short aliases R and QR are provided for brevity.

Testing the Solution

Various scenarios are demonstrated:

Fetching a non‑existent licence triggers LICENCE_NOT_FOUND.

Providing an invalid licence type triggers BAD_LICENCE_TYPE.

Accessing an undefined URL results in a 404 handled by handleServletException.

Method‑not‑allowed, missing parameters, and type mismatches are all captured and returned with appropriate codes.

When a database column is missing, the unknown exception handler returns a generic server‑error code, and in production mode the message is replaced with a user‑friendly "Network error".

All tests show that the unified handler returns consistent code / message JSON payloads.

Conclusion

By combining @ControllerAdvice, a custom Assert utility, and enum‑based error definitions, most exceptions in a Spring backend can be handled centrally, reducing boilerplate, improving readability, and providing a uniform API contract. The approach can be packaged as a common library for reuse across projects.

Ugly try‑catch code block
Ugly try‑catch code block
Elegant controller
Elegant controller
Exception stages diagram
Exception stages diagram
Assert usage
Assert usage
404 handling
404 handling
Production environment network error
Production environment network error
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.

BackendAssertControllerAdvice
Java Interview Crash Guide
Written by

Java Interview Crash Guide

Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.

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.