Mastering Unified Exception Handling in Spring Boot: Clean Code with Assertions

This article explains how to replace repetitive try‑catch blocks with a unified exception handling framework in Spring Boot, using @ControllerAdvice, custom Assert utilities, and enum‑based error codes, while showing concrete code examples, configuration steps, and runtime results.

Architect
Architect
Architect
Mastering Unified Exception Handling in Spring Boot: Clean Code with Assertions

During Java backend development it is common to litter the code with try { … } catch { … } finally { … } blocks, which makes the source noisy and hard to read. The author first demonstrates the problem with two screenshots of a "ugly" controller full of try‑catch statements and a cleaner version that delegates error handling.

To solve this, the article introduces Spring 3.2’s @ControllerAdvice together with @ExceptionHandler. These annotations allow a single class to intercept exceptions thrown from any controller, eliminating the need for duplicated error‑handling code in each controller or service.

Replacing try‑catch with assertions

The author proposes using assertions to validate business conditions instead of explicit if checks that throw exceptions. Spring’s org.springframework.util.Assert is shown as a reference:

public abstract class Assert {
    public static void notNull(Object object, String message) {
        if (object == null) {
            throw new IllegalArgumentException(message);
        }
    }
}

A custom Assert interface is then defined, providing default methods that create a BaseException when the assertion fails:

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 are introduced to hold error codes and messages. ResponseEnum defines business‑specific errors, while CommonResponseEnum and ArgumentResponseEnum cover generic cases such as server errors or validation failures:

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

The core of the solution is the UnifiedExceptionHandler class annotated with @ControllerAdvice. It defines several @ExceptionHandler methods, each returning a structured ErrorResponse containing a numeric code and a human‑readable message:

@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
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)) {
            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("Parameter binding validation exception", e);
        return wrapperBindingResult(e.getBindingResult());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
        log.error("Parameter binding validation exception", 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 categorises exceptions into three groups: (1) pre‑controller errors such as 404 or unsupported HTTP methods, (2) custom business exceptions, and (3) unknown runtime exceptions. For production environments the handler masks detailed stack traces and returns generic messages like "Network error".

Applying the pattern in a real service

An example LicenceService shows how to use the custom Assert and enum‑based errors:

@Service
public class LicenceService extends ServiceImpl<LicenceMapper, Licence> {
    @Autowired
    private OrganizationClient organizationClient;

    public LicenceDTO queryDetail(Long licenceId) {
        Licence licence = this.getById(licenceId);
        checkNotNull(licence);
        OrganizationDTO org = ClientUtil.execute(() -> organizationClient.getOrganization(licence.getOrganizationId()));
        return toLicenceDTO(licence, org);
    }

    public QueryData<SimpleLicenceDTO> getLicences(LicenceParam licenceParam) {
        String licenceType = licenceParam.getLicenceType();
        LicenceTypeEnum licenceTypeEnum = LicenceTypeEnum.parseOfNullable(licenceType);
        // assert non‑null
        ResponseEnum.BAD_LICENCE_TYPE.assertNotNull(licenceTypeEnum);
        LambdaQueryWrapper<Licence> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Licence::getLicenceType, licenceType);
        IPage<Licence> page = this.page(new QueryPage<>(licenceParam), wrapper);
        return new QueryData<>(page, this::toSimpleLicenceDTO);
    }

    @Transactional(rollbackFor = Throwable.class)
    public LicenceAddRespData addLicence(LicenceAddRequest request) {
        Licence licence = new Licence();
        licence.setOrganizationId(request.getOrganizationId());
        licence.setLicenceType(request.getLicenceType());
        licence.setProductName(request.getProductName());
        licence.setLicenceMax(request.getLicenceMax());
        licence.setLicenceAllocated(request.getLicenceAllocated());
        licence.setComment(request.getComment());
        this.save(licence);
        return new LicenceAddRespData(licence.getLicenceId());
    }

    private void checkNotNull(Licence licence) {
        ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
    }
    // conversion methods omitted for brevity
}

Database seed statements illustrate the initial data set, and a series of screenshots demonstrate how the application returns structured error responses for missing licences, invalid licence types, 404 routes, unsupported HTTP methods, and validation failures.

Configuration for 404 as an exception

To make Spring throw NoHandlerFoundException for unmapped URLs, the following properties are added to application.properties:

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

After this change, a request to a non‑existent endpoint results in a captured SERVER_ERROR response that the front‑end can translate into a user‑friendly 404 page.

Result and best practices

The final summary states that by combining assertions, enum‑based error codes, and a global @ControllerAdvice handler, most exception scenarios are covered with minimal boilerplate. The author notes that additional concerns such as security, gateway fallback, or remote‑call errors would require separate handlers, but the presented pattern forms a solid foundation for clean, maintainable backend code.

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 HandlingenumSpring BootassertionsUnified Error Handling
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.