Post Thumbnail

Spring Boot Exception Handling

1. Overview

Exceptions are undesired behaviour 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "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 recognise our error endpoint.

Listing 2.1 GlobalExceptionHandler.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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

1
2
3
4
5
6
7
8
9
@RestController
public class IndexController {

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

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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

1
2
3
4
5
6
7
8
9
@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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@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));
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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 o 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.

Love this concise and interesting article about exception handling? You should totally check out my book on Spring Cloud OpenFeign. You’ll never call external APIs the same way again.