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.
Join the DZone community and get the full member experience.
Join For Free1. 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
{
"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
@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:
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
@RestController
public class IndexController {
@GetMapping("/ex/runtime")
public ResponseEntity<Map<String, Object>> runtimeException() {
throw new RuntimeException("RuntimeException raised");
}
}
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
@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:
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
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
@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
@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));
}
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.
Published at DZone with permission of Seun Matt. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments