How to Eliminate 95% of Try‑Catch Blocks with Unified Exception Handling in Spring

This article explains how to replace repetitive try‑catch blocks in Spring applications by using @ControllerAdvice, custom Assert utilities, and enum‑based exception definitions, providing a clean, unified error‑handling mechanism with consistent response structures and environment‑aware messaging.

ITPUB
ITPUB
ITPUB
How to Eliminate 95% of Try‑Catch Blocks with Unified Exception Handling in Spring

Background

In Java development, handling exceptions often leads to abundant try { … } catch { … } finally { … } blocks, which make code verbose and hard to read. The article compares a messy controller with many try‑catch statements to a clean, elegant one, and argues for a strategy that removes most of these blocks.

What Is Unified Exception Handling?

Spring 3.2 introduced @ControllerAdvice, which can be combined with @ExceptionHandler, @InitBinder, and @ModelAttribute. By annotating a class with @ControllerAdvice, you can define exception‑handling methods that apply to all controllers, avoiding duplicated handling logic in each controller.

Exceptions are categorized into two stages: before entering the controller and within the service layer.

Goal

Eliminate more than 95% of try‑catch blocks by using an elegant Assert (assertion) approach to validate business logic, allowing developers to focus on core functionality.

Unified Exception Handling in Practice

Replace throw with Assert

Spring provides org.springframework.util.Assert. The article shows a JUnit test using Assert.notNull(user, "User not found.") and compares it with a traditional

if (user == null) { throw new IllegalArgumentException("User not found."); }

approach.

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

@Test
public void test2(){
    // 另一种写法
    User user = userDao.selectById(userId);
    if (user == null) {
        throw new IllegalArgumentException("用户不存在.");
    }
}

The Assert class simply wraps the null‑check and throws an exception when the condition fails. The article then defines a custom Assert interface that creates 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);
        }
    }
}

Two enums implement this interface, providing error codes and messages:

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

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
}

Using the enum, you can write concise validation code such as:

ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);

Enum‑Based Exception Definition

The enum holds a code and a message, similar to typical response enums. A BusinessException class extends BaseException and receives the enum, arguments, and formatted message.

public class BusinessException extends BaseException {
    public BusinessException(IResponseEnum responseEnum, Object[] args, String message) {
        super(responseEnum, args, message);
    }
    // other constructors omitted
}

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 servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName());
            code = servletExceptionEnum.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 baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
            String message = getMessage(baseException);
            return new ErrorResponse(code, message);
        }
        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 baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
            String message = getMessage(baseException);
            return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), message);
        }
        return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
    }
}

The handler groups exceptions into three categories: pre‑controller (e.g., 404, method not supported), custom business exceptions, and unknown exceptions. In production, detailed messages are hidden and a generic “network error” is returned.

Handling 404 as an Exception

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

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

Now NoHandlerFoundException can be caught by handleServletException and a unified error response is sent to the front‑end.

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 contains totalCount, pageNo, pageSize, and records.

Validation Example

In a service method, the enum‑based Assert validates input:

ResponseEnum.BAD_LICENCE_TYPE.assertNotNull(licenceTypeEnum);

If validation fails, the corresponding BusinessException (e.g., BAD_LICENCE_TYPE) is thrown and handled uniformly.

Testing the Unified Handler

Request a non‑existent licence ID ( /licence/5) → receives LICENCE_NOT_FOUND error.

Request a licence list with an invalid type ( /licence/list?licenceType=ddd) → receives BAD_LICENCE_TYPE error.

Access an undefined endpoint ( /licence/list/ddd) → 404 is captured as NoHandlerFoundException.

Send a request with an unsupported HTTP method → HttpRequestMethodNotSupportedException is captured.

Submit a request with missing required parameters → ArgumentResponseEnum.VALID_ERROR aggregates field‑level messages.

Trigger an unknown exception by adding a new field to the entity without updating the DB schema → database error is caught by the generic handler.

All these scenarios return a JSON object containing code and message, demonstrating that the unified approach consistently handles diverse error conditions.

Production‑Environment Considerations

When the active profile is prod, the handler replaces detailed exception messages with a generic "Network error" to avoid exposing internal details.

Internationalization

The getMessage method looks up a message key response.{enumName} in a message source, allowing error messages to be localized based on the request locale.

Conclusion

By combining custom Assert utilities, enum‑based error codes, and a @ControllerAdvice handler, most try‑catch blocks can be removed, resulting in cleaner code and a consistent error‑response contract. The approach is extensible to additional exception types such as security or gateway errors.

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 HandlingspringAssertControllerAdvice
ITPUB
Written by

ITPUB

Official ITPUB account sharing technical insights, community news, and exciting events.

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.