Mastering Custom Exception Handling in Spring Boot: A Complete Guide

This article walks through Spring Boot's default error mapping, shows how to return JSON for AJAX requests and HTML pages for browsers, and provides step‑by‑step code for creating custom exception classes, an error response entity, a Freemarker error template, and a global @ControllerAdvice handler with detailed configuration and testing examples.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Mastering Custom Exception Handling in Spring Boot: A Complete Guide

Preface

When the application starts, Spring Boot registers two mappings for {[/error]} . When an exception occurs, Spring Boot forwards to /error and renders either JSON or an HTML page depending on the request type.
2018-12-18 09:36:24.627  INFO 19040 --- [main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" ...
2018-12-18 09:36:24.632  INFO 19040 --- [main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" ...

Spring Boot basics are omitted; see the recommended best‑practice repository for a full tutorial.

Default Exception Handling

When an AJAX request triggers an error, Spring Boot returns JSON:

{
    "timestamp": "2018-12-18T01:50:51.196+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No handler found for GET /err404",
    "path": "/err404"
}

When a normal browser request triggers an error, an HTML page is rendered:

Custom Exception Handling

Add Dependencies

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.54</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
fastjson

provides JSON serialization, while spring-boot-starter-freemarker supplies the template engine for rendering error pages.

Add Configuration

# Throw exception when no handler is found (so 404 can be caught)
spring.mvc.throw-exception-if-no-handler-found=true
# Disable automatic resource mappings
spring.resources.add-mappings=false
spring:
  mvc:
    throw-exception-if-no-handler-found: true
  resources:
    add-mappings: false

Create Error Response Entity

/**
 * Information entity
 */
public class ExceptionEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    private String message;
    private int code;
    private String error;
    private String path;
    @JSONField(format = "yyyy-MM-dd hh:mm:ss")
    private Date timestamp = new Date();
    // getters and setters omitted for brevity
}

Create Custom Exceptions

/** Custom base exception */
public class BasicException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    private int code = 0;
    public BasicException(int code, String message) {
        super(message);
        this.code = code;
    }
    public int getCode() { return this.code; }
}
/** Business exception used in the application */
public class BusinessException extends BasicException {
    private static final long serialVersionUID = 1L;
    public BusinessException(int code, String message) {
        super(code, message);
    }
}

Create error.ftl Template (placed in /src/main/resources/templates/ )

<!DOCTYPE html>
<html>
<head>
    <meta name="robots" content="noindex,nofollow" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
    <style>
        h2{color:#4288ce;font-weight:400;padding:6px 0;margin:6px 0 0;font-size:18px;border-bottom:1px solid #eee;}
        .exception-var table{width:100%;max-width:500px;margin:12px 0;box-sizing:border-box;table-layout:fixed;word-wrap:break-word;}
        .exception-var table caption{font-size:16px;font-weight:bold;text-align:left;padding:6px 0;}
        .exception-var table tbody{font-size:13px;font-family:Consolas,"Liberation Mono",Courier,"微软雅黑";}
        .exception-var table td{padding:0 6px;vertical-align:top;word-break:break-all;}
        .exception-var table td:first-child{width:28%;font-weight:bold;white-space:nowrap;}
        .exception-var table td pre{margin:0;}
    </style>
</head>
<body>
    <div class="exception-var">
        <h2>Exception Data</h2>
        <table>
            <tbody>
                <tr><td>Code</td><td>${(exception.code)!}</td></tr>
                <tr><td>Time</td><td>${(exception.timestamp?datetime)!}</td></tr>
                <tr><td>Path</td><td>${(exception.path)!}</td></tr>
                <tr><td>Exception</td><td>${(exception.error)!}</td></tr>
                <tr><td>Message</td><td>${(exception.message)!}</td></tr>
            </tbody>
        </table>
    </div>
</body>
</html>

Write Global Exception Handler

@ControllerAdvice
public class GlobalExceptionHandler {
    /** 404 */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ModelAndView errorHandler(HttpServletRequest request, NoHandlerFoundException ex, HttpServletResponse response) {
        return commonHandler(request, response, ex.getClass().getSimpleName(), HttpStatus.NOT_FOUND.value(), ex.getMessage());
    }
    /** 405 */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ModelAndView errorHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException ex, HttpServletResponse response) {
        return commonHandler(request, response, ex.getClass().getSimpleName(), HttpStatus.METHOD_NOT_ALLOWED.value(), ex.getMessage());
    }
    /** 415 */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public ModelAndView errorHandler(HttpServletRequest request, HttpMediaTypeNotSupportedException ex, HttpServletResponse response) {
        return commonHandler(request, response, ex.getClass().getSimpleName(), HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), ex.getMessage());
    }
    /** 500 */
    @ExceptionHandler(Exception.class)
    public ModelAndView errorHandler(HttpServletRequest request, Exception ex, HttpServletResponse response) {
        return commonHandler(request, response, ex.getClass().getSimpleName(), HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage());
    }
    /** Business exception */
    @ExceptionHandler(BasicException.class)
    private ModelAndView errorHandler(HttpServletRequest request, BasicException ex, HttpServletResponse response) {
        return commonHandler(request, response, ex.getClass().getSimpleName(), ex.getCode(), ex.getMessage());
    }
    /** Validation exception */
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public ExceptionEntity validExceptionHandler(BindException ex, HttpServletRequest request, HttpServletResponse response) {
        Map<String, String> errors = new HashMap<>();
        for (FieldError err : ex.getBindingResult().getFieldErrors()) {
            errors.put(err.getField(), err.getDefaultMessage());
        }
        ExceptionEntity entity = new ExceptionEntity();
        entity.setMessage(JSON.toJSONString(errors));
        entity.setPath(request.getRequestURI());
        entity.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        entity.setError(ex.getClass().getSimpleName());
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        return entity;
    }
    private ModelAndView commonHandler(HttpServletRequest request, HttpServletResponse response, String error, int httpCode, String message) {
        ExceptionEntity entity = new ExceptionEntity();
        entity.setPath(request.getRequestURI());
        entity.setError(error);
        entity.setCode(httpCode);
        entity.setMessage(message);
        return determineOutput(request, response, entity);
    }
    private ModelAndView determineOutput(HttpServletRequest request, HttpServletResponse response, ExceptionEntity entity) {
        if (!(request.getHeader("accept").contains("application/json") ||
              (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").contains("XMLHttpRequest")))) {
            ModelAndView mv = new ModelAndView("error");
            mv.addObject("exception", entity);
            return mv;
        } else {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setCharacterEncoding("UTF8");
            response.setHeader("Content-Type", "application/json");
            try {
                response.getWriter().write(ResultJsonTools.build(
                        ResponseCodeConstant.SYSTEM_ERROR,
                        ResponseMessageConstant.APP_EXCEPTION,
                        JSONObject.parseObject(JSON.toJSONString(entity))
                ));
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
}

Annotations Explained @ControllerAdvice: Marks a class as a global exception handler. @ExceptionHandler: Specifies which exception types the method handles. BindException: Thrown by Hibernate Validator when form validation fails.

Test Controller

@RestController
public class TestController {
    @RequestMapping("err")
    public void error() {
        throw new BusinessException(400, "业务异常错误信息");
    }
    @RequestMapping("err2")
    public void error2() {
        throw new NullPointerException("手动抛出异常信息");
    }
    @RequestMapping("err3")
    public int error3() {
        int a = 10 / 0;
        return a;
    }
}

Sample JSON responses for AJAX requests:

# /err
{
    "msg": "应用程序异常",
    "code": -1,
    "status_code": 0,
    "data": {
        "path": "/err",
        "code": 400,
        "error": "BusinessException",
        "message": "业务异常错误信息",
        "timestamp": "2018-12-18 11:09:00"
    }
}
# /err2
{ ... }
# /err3
{ ... }
# /err404
{ ... }

Sample HTML page rendered for browser requests (screenshots omitted for brevity).

Reference project: spring‑cloud‑zuul example
Further reading: 《微服务 分布式架构开发实战》 龚鹏 著; Jianshu article
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.

JavaException HandlingSpring BootCustom ExceptionGlobal Exception
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

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.