Graceful Global Exception Handling in SpringBoot

Introduction

This article primarily explains how to implement global exception handling in a SpringBoot project.

Preparation for SpringBoot Global Exception Handling

Note: If you want to directly obtain the project, you can skip to the bottom and download the project code via the link.

Development Prerequisites

Environment Requirements

  • JDK: 1.8
  • SpringBoot: 1.5.17.RELEASE

First, the Maven dependencies:

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.17.RELEASE</version>
    <relativePath />
</parent>
<dependencies>
    <!-- Spring Boot Web dependency (core) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Boot Test dependency -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.41</version>
    </dependency>
</dependencies>

The configuration file does not need many changes; global exception handling can be achieved directly in the code.

Code Implementation

While SpringBoot projects already have basic exception handling, it is often not suitable for developers. Therefore, we need to uniformly catch and handle these exceptions. SpringBoot provides the @ControllerAdvice annotation, which enables global exception catching. We can define a custom method with the @ExceptionHandler annotation and specify the exception type to handle these caught exceptions uniformly.

Let's look at how to use this annotation through the following example.

Example Code:

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public String exceptionHandler(Exception e) {
        System.out.println("Unknown exception! Cause:" + e);
        return e.getMessage();
    }
}

In the example above, we perform simple secondary processing on the caught exception and return the exception information. While this allows us to know the cause of the exception, it may not be user-friendly enough. Therefore, we can implement a more suitable format using custom exception classes and enums.

Custom Base Interface

First, define a base interface that custom error description enums must implement.

Code:

public interface BaseErrorInfoInterface {
    /** Get the error code */
    String getResultCode();

    /** Get the error description */
    String getResultMsg();
}

Custom Enum

Next, create a custom enum that implements this interface.

Code:

public enum CommonEnum implements BaseErrorInfoInterface {
    // Data operation error definitions
    SUCCESS("200", "Success!"),
    BODY_NOT_MATCH("400", "Request data format mismatch!"),
    SIGNATURE_NOT_MATCH("401", "Request digital signature mismatch!"),
    NOT_FOUND("404", "Resource not found!"),
    INTERNAL_SERVER_ERROR("500", "Internal server error!"),
    SERVER_BUSY("503", "Server busy, please try again later!");

    /** Error code */
    private final String resultCode;

    /** Error description */
    private final String resultMsg;

    CommonEnum(String resultCode, String resultMsg) {
        this.resultCode = resultCode;
        this.resultMsg = resultMsg;
    }

    @Override
    public String getResultCode() {
        return resultCode;
    }

    @Override
    public String getResultMsg() {
        return resultMsg;
    }
}

Custom Exception Class

Now, create a custom exception class to handle business exceptions.

Code:

public class BizException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    /** Error code */
    protected String errorCode;

    /** Error message */
    protected String errorMsg;

    public BizException() {
        super();
    }

    public BizException(BaseErrorInfoInterface errorInfoInterface) {
        super(errorInfoInterface.getResultCode());
        this.errorCode = errorInfoInterface.getResultCode();
        this.errorMsg = errorInfoInterface.getResultMsg();
    }

    public BizException(BaseErrorInfoInterface errorInfoInterface, Throwable cause) {
        super(errorInfoInterface.getResultCode(), cause);
        this.errorCode = errorInfoInterface.getResultCode();
        this.errorMsg = errorInfoInterface.getResultMsg();
    }

    public BizException(String errorMsg) {
        super(errorMsg);
        this.errorMsg = errorMsg;
    }

    public BizException(String errorCode, String errorMsg) {
        super(errorCode);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public BizException(String errorCode, String errorMsg, Throwable cause) {
        super(errorCode, cause);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public String getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }

    @Override
    public String getMessage() {
        return errorMsg;
    }

    @Override
    public Throwable fillInStackTrace() {
        return this;
    }
}

Custom Response Format

Define a unified response format for data transfer.

Code:

public class ResultBody {
    /** Response code */
    private String code;

    /** Response message */
    private String message;

    /** Response data */
    private Object result;

    public ResultBody() {
    }

    public ResultBody(BaseErrorInfoInterface errorInfo) {
        this.code = errorInfo.getResultCode();
        this.message = errorInfo.getResultMsg();
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Object getResult() {
        return result;
    }

    public void setResult(Object result) {
        this.result = result;
    }

    /** Success response */
    public static ResultBody success() {
        return success(null);
    }

    /** Success response with data */
    public static ResultBody success(Object data) {
        ResultBody rb = new ResultBody();
        rb.setCode(CommonEnum.SUCCESS.getResultCode());
        rb.setMessage(CommonEnum.SUCCESS.getResultMsg());
        rb.setResult(data);
        return rb;
    }

    /** Error response with error info interface */
    public static ResultBody error(BaseErrorInfoInterface errorInfo) {
        ResultBody rb = new ResultBody();
        rb.setCode(errorInfo.getResultCode());
        rb.setMessage(errorInfo.getResultMsg());
        rb.setResult(null);
        return rb;
    }

    /** Error response with custom code and message */
    public static ResultBody error(String code, String message) {
        ResultBody rb = new ResultBody();
        rb.setCode(code);
        rb.setMessage(message);
        rb.setResult(null);
        return rb;
    }

    /** Error response with default code */
    public static ResultBody error(String message) {
        ResultBody rb = new ResultBody();
        rb.setCode("-1");
        rb.setMessage(message);
        rb.setResult(null);
        return rb;
    }

    @Override
    public String toString() {
        return JSONObject.toJSONString(this);
    }
}

Custom Global Exception Handler

Finally, create a custom global exception handler class.

Code:

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /** Handle custom business exceptions */
    @ExceptionHandler(value = BizException.class)
    @ResponseBody
    public ResultBody handleBizException(HttpServletRequest req, BizException e) {
        logger.error("Business exception occurred! Reason: {}", e.getErrorMsg());
        return ResultBody.error(e.getErrorCode(), e.getErrorMsg());
    }

    /** Handle null pointer exceptions */
    @ExceptionHandler(value = NullPointerException.class)
    @ResponseBody
    public ResultBody handleNullPointerException(HttpServletRequest req, NullPointerException e) {
        logger.error("Null pointer exception occurred!", e);
        return ResultBody.error(CommonEnum.BODY_NOT_MATCH);
    }

    /** Handle other exceptions */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ResultBody handleException(HttpServletRequest req, Exception e) {
        logger.error("Unknown exception!", e);
        return ResultBody.error(CommonEnum.INTERNAL_SERVER_ERROR);
    }
}

Since the focus is on implementing global exception handling and its testing, we only need to add an entity class and a controller class.

Entity Class

Again, a generic user entity.

Code:

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private int id;
    private String name;
    private int age;

    public User() {}

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    @Override
    public String toString() {
        return JSONObject.toJSONString(this);
    }
}

Controller Layer

The controller is straightforward, implementing basic CRUD with RESTful style. However, some exceptions are deliberately introduced to test global exception handling. These include custom exceptions, null pointer exceptions, and an unexpected exception (e.g., type conversion exception).

Code:

@RestController
@RequestMapping(value = "/api")
public class UserRestController {

    @PostMapping("/user")
    public boolean insert(@RequestBody User user) {
        System.out.println("Starting insert...");
        if (user.getName() == null) {
            throw new BizException("-1", "User name cannot be empty!");
        }
        return true;
    }

    @PutMapping("/user")
    public boolean update(@RequestBody User user) {
        System.out.println("Starting update...");
        // Deliberately cause null pointer exception
        String str = null;
        str.equals("111");
        return true;
    }

    @DeleteMapping("/user")
    public boolean delete(@RequestBody User user) {
        System.out.println("Starting delete...");
        // Deliberately cause an exception (NumberFormatException)
        Integer.parseInt("abc123");
        return true;
    }

    @GetMapping("/user")
    public List<User> findByUser(User user) {
        System.out.println("Starting query...");
        List<User> userList = new ArrayList<>();
        User user2 = new User();
        user2.setId(1);
        user2.setName("xuwujing");
        user2.setAge(18);
        userList.add(user2);
        return userList;
    }
}

Application Antry

The main entry class is typical for a SpringBoot project.

Code:

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
        System.out.println("Application is running...");
    }
}

Functional Testing

After starting the application, use Postman for API testing.

First, test query functionality with a GET request.

GET http://localhost:8181/api/user

Response:

{"code":"200","message":"Success!","result":[{"id":1,"name":"xuwujing","age":18}]}

The program works correctly without interference from global exception handling.

Next, test if custom exceptions are properly caught and handled.

Use POST request.

POST http://localhost:8181/api/user

Body (JSON):

{"id":1}

Response:

{"code":"-1","message":"User name cannot be empty!","result":null}

The custom exception is caught and processed to return a formatted error response.

Now, test null pointer exception handling. In the global exception handler, we have both a specific handler for NullPointerException and a generic handler for Exception. Which one will be triggered?

Use PUT request.

PUT http://localhost:8181/api/user

Body (JSON):

{"id":1,"name":"xuwujing","age":18}

Response:

{"code":"400","message":"Request data format mismatch!","result":null}

The null pointer exception is caught by its specific hendler, showing that global exception handling prioritizes the most specific handler.

Finally, test an unspecified exception (e.g., NumberFormatException).

Use DELETE request.

DELETE http://localhost:8181/api/user

Body (JSON):

{"id":1}

Response:

{"code":"500","message":"Internal server error!","result":null}

The generic Exception handler catches this exception.

All tests are successful. Additionally, global exception handling can also be used for page redirection by removing the @ResponseBody annotation and returning a view path. Note that GlobalExceptionHandler uses @ControllerAdvice, not @RestControllerAdvice. If @RestControllerAdvice is used, the response will be automatically converted to JSON format (similar to @RestController vs @Controller). The choice depends on your needs.

Other Information

The explanation of graceful global exception handling in SpringBoot is complete. Please feel free to correct any errors.

Project Repository

SpringBoot global exception handling project:

Complete SpringBoot study collection:

Tags: SpringBoot Global Exception Handling ControllerAdvice ExceptionHandler java

Posted on Fri, 08 May 2026 20:42:24 +0000 by arcanine