Mastering Unified Exception Handling in Spring: Clean Code, Better Errors

This article explains how to replace noisy try‑catch blocks with Spring's @ControllerAdvice and custom Assert utilities, using enums for error codes, unified response structures, and practical MyBatis‑Plus examples to achieve clean, maintainable backend exception handling.

Java Backend Technology
Java Backend Technology
Java Backend Technology
Mastering Unified Exception Handling in Spring: Clean Code, Better Errors

In software development, handling exceptions often results in repetitive try { ... } catch { ... } finally { ... } blocks that clutter code and reduce readability.

What Is Unified Exception Handling

Spring 3.2 added the @ControllerAdvice annotation, which works together with @ExceptionHandler, @InitBinder, and @ModelAttribute to apply exception handling across all controllers, eliminating the need to duplicate handlers in each controller class.

Goal

The aim is to eliminate more than 95% of explicit try catch blocks and replace them with Assert (assertion) checks that focus on business logic while automatically throwing appropriate exceptions.

Unified Exception Handling in Practice

Using Assert (Assertion) Instead of Throwing Exceptions

Spring provides org.springframework.util.Assert for concise null checks and other validations. Example test methods illustrate the readability difference between Assert.notNull(user, "User not found.") and a manual

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

check.

@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 class simply wraps the null‑check and throws an IllegalArgumentException when the condition fails.

Enum‑Based Exception Definitions

Instead of creating a separate exception class for each error scenario, define an enum that holds an error code and message. Implement a BaseException that receives the enum and formats the final 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);
    }
    // other constructors omitted
}

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, validation becomes a one‑liner:

ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);

Unified Exception Handler Class

The UnifiedExceptionHandler class is annotated with @ControllerAdvice and defines multiple @ExceptionHandler methods to process business exceptions, generic base exceptions, servlet‑related exceptions, binding/validation errors, and any other unknown exceptions. It also distinguishes production and development environments to hide internal messages in production.

@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(value = BusinessException.class)
    @ResponseBody
    public ErrorResponse handleBusinessException(BaseException e) {
        log.error(e.getMessage(), e);
        return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
    }

    // other handlers omitted for brevity
}

The handler groups exceptions into three categories: pre‑controller servlet exceptions, custom business/base exceptions, and unknown exceptions.

Exception Classification Diagram

Exception classification diagram
Exception classification diagram

Typical Servlet Exceptions Handled

NoHandlerFoundException – 404 not found

HttpRequestMethodNotSupportedException – wrong HTTP method

HttpMediaTypeNotSupportedException – unsupported Content‑Type

MissingPathVariableException – missing @PathVariable

MissingServletRequestParameterException – missing request parameter

TypeMismatchException – type conversion failure

HttpMessageNotReadableException – request body cannot be parsed

HttpMessageNotWritableException – response body cannot be serialized

Unified Response Structure

All API responses share a base class BaseResponse containing code and message. Successful responses extend it with a data field (e.g., CommonResponse, QueryDataResponse). Helper classes R and QR provide concise constructors like new R<>(data) or new QR<>(queryData).

Verification Example

A sample Spring Boot project using MyBatis‑Plus demonstrates the unified handling. Service methods such as queryDetail(Long licenceId), getLicences(LicenceParam licenceParam), and addLicence(LicenceAddRequest request) use ResponseEnum assertions to validate inputs. The article includes screenshots of API calls that trigger various exceptions (missing licence, bad licence type, 404, unsupported method, validation errors, database errors) and shows the consistent JSON error format returned.

Missing licence error
Missing licence error
Licence not found
Licence not found
Licence not found message
Licence not found message

Other screenshots illustrate 404 handling, method‑not‑supported errors, validation failures, and database exceptions, confirming that every error is captured and returned with a unified code / message payload.

Production‑Environment Considerations

When the active profile is prod, the handler replaces detailed internal messages with generic ones (e.g., "Network error") to avoid exposing stack traces to end users.

Conclusion

Combining assertions, enum‑based error definitions, and a centralized @ControllerAdvice provides a clean, maintainable way to handle most backend exceptions in Spring applications. For advanced scenarios such as security, gateway fallback, or remote‑call failures, additional handlers may be required.

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 Handlingspringenummybatis-plusAssert
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.