DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • How To Build Web Service Using Spring Boot 2.x
  • How To Run the Spring Boot Application as a Stand-Alone Java Application

Trending

  • Why DDoS Protection Is an Architectural Decision for Developers
  • When One MVP Is Really Four Systems: A Better Way to Plan Multi-Role Apps
  • OpenAPI From Code With Spring and Java: A Recipe for Your CI
  • From Indicators to Insights: Automating IOC Enrichment Using Python and Threat Feeds
  1. DZone
  2. Coding
  3. Frameworks
  4. Spring Boot Exception Handling

Spring Boot Exception Handling

In this article, we are going to learn how to configure Spring Boot to handle exceptions, including custom ones, and customize the error responses and handling.

By 
Seun Matt user avatar
Seun Matt
DZone Core CORE ·
Nov. 22, 21 · Tutorial
Likes (8)
Comment
Save
Tweet
Share
15.0K Views

Join the DZone community and get the full member experience.

Join For Free

1. Overview

Exceptions are undesired behavior of a software application caused by faulty logic. In this article, we're going to be looking at how to handle exceptions in a Spring Boot application.

What we want to achieve is that whenever there's an error in our application, we want to gracefully handle it and return the following response format:

Listing 1.1 error response format 

JSON
 
{
  "code": 400,
  "message": "Missing required fields",
  "errors": [
    "additional specific error"
    "username is required",
    "email is required"
  ],
  "status": false
}


The code is a standard HTTP status code and the status attribute is a simple way to know if the request is successful or not. The response message is a summary of the failures while the errors array contains more specific and detailed error messages.

2. Basic Exception Handling

We will create a class GlobalExceptionHandler that will implement the ErrorController interface and define a controller action for the /error endpoint. We will annotate the class with @RestController for Spring Boot to recognize our error endpoint.

Listing 2.1 GlobalExceptionHandler.java

Java
 
@RestController
public class GlobalExceptionHandler implements ErrorController {

    public GlobalExceptionHandler() {
    }

    @RequestMapping("/error")
    public ResponseEntity<Map<String, Object>> handleError(HttpServletRequest request) {
        HttpStatus httpStatus = getHttpStatus(request);
        String message = getErrorMessage(request, httpStatus);

        Map<String, Object> response = new HashMap<>();
        response.put("status", false);
        response.put("code", httpStatus.value());
        response.put("message", message);
        response.put("errors", Collections.singletonList(message));

        return ResponseEntity.status(httpStatus).body(response);
    }

    private HttpStatus getHttpStatus(HttpServletRequest request) {

        //get the standard error code set by Spring Context
        Integer status = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        if (status != null) {
            return HttpStatus.valueOf(status);
        }

        // maybe we're the one that trigger the redirect
        // with the code param
        String code = request.getParameter("code");
        if (code != null && !code.isBlank()) {
            return HttpStatus.valueOf(code);
        }

        //default fallback
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }

    private String getErrorMessage(HttpServletRequest request, HttpStatus httpStatus) {

        //get the error message set by Spring context
        // and return it if it's not null
        String message = (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
        if (message != null && !message.isEmpty()) {
            return message;
        }

        //if the default message is null,
        //let's construct a message based on the HTTP status
        switch (httpStatus) {
            case NOT_FOUND:
                message = "The resource does not exist";
                break;
            case INTERNAL_SERVER_ERROR:
                message = "Something went wrong internally";
                break;
            case FORBIDDEN:
                message = "Permission denied";
                break;
            case TOO_MANY_REQUESTS:
                message = "Too many requests";
                break;
            default:
                message = httpStatus.getReasonPhrase();
        }

        return message;
    }

}


We created two helper functions — getHttpStatus and getErrorMessage. The first one will extract the HTTP status from the Servlet request while the second function will extrapolate the error message from either the Servlet request or the HTTP status.

The handleError function will be called whenever there's a runtime error in the application. The function will use the two helper methods to get the code and message to return as part of the final response.

Let's run the application and use curl to test our setup. We will simply visit an endpoint that does not exist:

Java
 
curl --location --request GET 'http://localhost:8080/not/found'

{
  "code": 404,
  "message": "The resource does not exist",
  "errors": [
    "The resource does not exist"
  ],
  "status": false
}


Our application is now returning our custom response. Let's add a new controller action that will raise a RuntimeException with a custom message and see what the response will be when we call it.

Listing 2.2 IndexController.java

Java
 
@RestController
public class IndexController {

    @GetMapping("/ex/runtime")
    public ResponseEntity<Map<String, Object>> runtimeException() {
        throw new RuntimeException("RuntimeException raised");
    }

}


Java
 
curl --location --request GET 'http://localhost:8080/ex/runtime' -v

*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> GET /ex/runtime HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.77.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 500 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Tue, 16 Nov 2021 07:10:10 GMT
< Connection: close
< 
* Closing connection 0

{
  "code": 500,
  "message": "Something went wrong internally",
  "errors": [
    "Something went wrong internally"
  ],
  "status": false
}


This time around, we appended the -v flag to the curl command and we can see from the verbose response that the HTTP code returned is indeed 500 — the same as the value of code in the returned response body.

3. Handling Specific Exception Class

Even though what we have is capable of handling all exceptions, we can still have specific handlers for specific exception classes.

At times we want to handle certain exception classes because we want to respond differently and/or execute custom logic. 

To achieve this, we will annotate the GlobalExceptionHandler class with @RestControllerAdvice and define exception handler methods for each exception class we want to handle.

For the purpose of this article, we will handle the HttpRequestMethodNotSupportedException class and return a custom message.

Listing 3.1 GlobalExceptionHandler.java

Java
 
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<Map<String, Object>> handleError(HttpRequestMethodNotSupportedException e) {
    String message = e.getMessage();
    Map<String, Object> response = new HashMap<>();
    response.put("status", false);
    response.put("code", HttpStatus.METHOD_NOT_ALLOWED);
    response.put("message", "It seems you're using the wrong HTTP method");
    response.put("errors", Collections.singletonList(message));
    return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(response);
}


Now, if we call the /ex/runtime endpoint with a POST method, we should get the unique message that we set and the errors array will contain the raw exception message:

Java
 
curl --location --request POST 'http://localhost:8080/ex/runtime'

{
  "code": 405,
  "message": "It seems you're using the wrong HTTP method",
  "errors": [
    "Request method 'POST' not supported"
  ],
  "status": false
}


We can repeat this for as many as possible exception classes that we want to handle specifically. Note that declaring a specific handler means the /error endpoint will not be invoked for that particular exception

4. Handling a Custom Exception Class

Simply put, we will create a subclass of the RuntimeException class and create a specific handler for it in the GlobalExceptionHandler. Whenever we want to return an error response to our API client, we will just raise a new instance of our custom exception class.

The sweet part is that we can throw the exception from a controller, a service, or just about any other component and it will be handled correctly.

First, let's create the custom exception class.

Listing 4.1 CustomApplicationException.java

Java
 
public class CustomApplicationException extends RuntimeException {

    private HttpStatus httpStatus;
    private List<String> errors;
    private Object data;

    public CustomApplicationException(String message) {
        this(HttpStatus.BAD_REQUEST, message);
    }

    public CustomApplicationException(String message, Throwable throwable) {
        super(message, throwable);
    }

    public CustomApplicationException(HttpStatus httpStatus, String message) {
        this(httpStatus, message, Collections.singletonList(message), null);
    }

    public CustomApplicationException(HttpStatus httpStatus, String message, Object data) {
        this(httpStatus, message, Collections.singletonList(message), data);
    }

    public CustomApplicationException(HttpStatus httpStatus, String message, List<String> errors) {
        this(httpStatus, message, errors, null);
    }

    public CustomApplicationException(HttpStatus httpStatus, String message, List<String> errors, Object data) {
        super(message);
        this.httpStatus = httpStatus;
        this.errors = errors;
        this.data = data;
    }

    public HttpStatus getHttpStatus() {
        return httpStatus;
    }

    public List<String> getErrors() {
        return errors;
    }

    public Object getData() {
        return data;
    }
}


We defined a number of useful fields for the CustomApplicationException class alongside convenient constructors. This means we can specify the HTTP status, message, and list of errors when we're raising the exception.

Now we will define a handler for it and create a controller endpoint to test it out.

Listing 4.2 GlobalExceptionHandler.java 

Java
 
@ExceptionHandler(CustomApplicationException.class)
public ResponseEntity<Map<String, Object>> handleError(CustomApplicationException e) {
    Map<String, Object> response = new HashMap<>();
    response.put("status", false);
    response.put("code", e.getHttpStatus().value());
    response.put("message", e.getMessage());
    response.put("errors", e.getErrors());
    return ResponseEntity.status(e.getHttpStatus()).body(response);
}


Listing 4.3 IndexController.java

Java
 
@PostMapping("/ex/custom")
public ResponseEntity<Map<String, Object>> customException(@RequestBody Map<String, Object> request) {
    List<String> errors = new ArrayList<>();
    if(!request.containsKey("username"))
        errors.add("Username is required");
    if(!request.containsKey("password"))
        errors.add("Password is required");

    if(!errors.isEmpty()) {
        String errorMessage = "Missing required parameters";
        throw new CustomApplicationException(HttpStatus.BAD_REQUEST, errorMessage , errors);
    }

    return ResponseEntity.ok(Collections.singletonMap("status", true));
}


Java
 
curl --location --request POST 'http://localhost:8080/ex/custom' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username": "john"
}'


{
    "code": 400,
    "message": "Missing required parameters",
    "errors": [
        "Password is required"
    ],
    "status": false
}


The returned message is a general description of what went wrong while the errors contain the exact field that's missing — just as we wanted.

5. Conclusion

We've looked at how to configure Spring Boot to handle different types of exceptions and return the desired response. In the next article, we're going to look at how we can apply these techniques to a monolith application and return HTML templates/responses.

The complete source code is available on GitHub.

Spring Framework Spring Boot Listing (computer) application Java (programming language)

Published at DZone with permission of Seun Matt. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • How To Build Web Service Using Spring Boot 2.x
  • How To Run the Spring Boot Application as a Stand-Alone Java Application

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook