Introduction
In this article, we will look into few approaches of exception handling in Spring REST applications.
This tutorial assumes that you have a basic knowledge of Spring and can create simple REST APIs using it.
If you'd like to read more about exceptions and custom exceptions in Java, we've covered it in detail in Exception Handling in Java: A Complete Guide with Best and Worst Practices and How to Make Custom Exceptions in Java.
Why Do It?
Suppose we have a simple user service where we can fetch and update registered users. We have a simple model defined for the users:
public class User {
private int id;
private String name;
private int age;
// Constructors, getters, and setters
Let's create a REST controller with a mapping that expects an id
and returns the User
with the given id
if present:
@RestController
public class UserController {
private static List<User> userList = new ArrayList<>();
static {
userList.add(new User(1, "John", 24));
userList.add(new User(2, "Jane", 22));
userList.add(new User(3, "Max", 27));
}
@GetMapping(value = "/user/{id}")
public ResponseEntity<?> getUser(@PathVariable int id) {
if (id < 0) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
User user = findUser(id);
if (user == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(user);
}
private User findUser(int id) {
return userList.stream().filter(user -> user.getId().equals(id)).findFirst().orElse(null);
}
}
Apart from just finding the user, we also have to perform additional checks, like the id
that's passed should always be greater than 0, otherwise we have to return a BAD_REQUEST
status code.
Similarly, if the user is not found then we have to return a NOT_FOUND
status code. Additionally, we might have to add text for some detail about the error to the client.
For each check, we have to create a ResponseEntity object having response codes and text according to our requirements.
We can easily see that these checks will have to be made multiple times as our APIs grows. For an example, suppose we are adding a new PATCH request mapping to update our users, we need to again create these ResponseEntity
objects. This creates the problem of maintaining consistencies within the app.
So the problem we are trying to solve is the separation of concerns. Of course, we have to perform these checks in each RequestMapping
but instead of handling validation/error scenarios and what responses need to be returned in each of them, we can simply throw an exception after a violation and these exceptions will be then handled separately.
Now, you can use built-in exceptions already provided by Java and Spring, or if needed you can create your own exceptions and throw them. This will also centralize our validation/error handling logic.
Additionally, we can't return default server error messages to the client when serving an API. Neither can we return stack traces that are convoluted and hard to understand for our clients. Proper exception handling with Spring is a very important aspect of building a good REST API.
Alongisde exception handling, REST API Documentation is a must.
Exception Handling via @ResponseStatus
The @ResponseStatus annotation can be used on methods and exception classes. It can be configured with a status code which would be applied to the HTTP response.
Let's create a custom exception to handle the situation when the user is not found. This will be a runtime exception hence we have to extend the java.lang.RuntimeException
class.
We'll also mark this class with @ResponseStatus
:
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "User Not found")
public class UserNotFoundException extends RuntimeException {
}
When Spring catches this Exception, it uses the configuration provided in @ResponseStatus
.
Changing our controller to use the same:
@GetMapping(value = "/user/{id}")
public ResponseEntity<?> getUser(@PathVariable int id) {
if (id < 0) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
User user = findUser(id);
return ResponseEntity.ok(user);
}
private User findUser(int id) {
return userList.stream().filter(user -> user.getId().equals(id)).findFirst().orElseThrow(() -> new UserNotFoundException());
}
As we can see, the code is cleaner now with separation of concerns.
@RestControllerAdvice and @ExceptionHandler
Let's create a custom exception to handle validation checks. This again will be a RuntimeException
:
public class ValidationException extends RuntimeException {
private static final long serialVersionUID = 1L;
private String msg;
public ValidationException(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
}
@RestControllerAdvice is a new feature of Spring that can be used to write common code for exception handling.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
This is usually used in conjunction with @ExceptionHandler that actually handles different exceptions:
@RestControllerAdvice
public class AppExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ValidationException.class)
public ResponseEntity<?> handleException(ValidationException exception) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getMsg());
}
}
You can think of RestControllerAdvice
as a sort of Aspect in your Spring code. Whenever your Spring code throws an exception which has a handler defined in this class, appropriate logic could be written according to business needs.
Notice that unlike @ResponseStatus
we could do many things with this approach, like logging our exceptions, notifying etc.
What if we wanted to update the age of an existing user? We have 2 validation checks that need to be made:
- The
id
must be greater than 0 - The
age
must be between 20 to 60
With that in mind, let's make an endpoint for just that:
@PatchMapping(value = "/user/{id}")
public ResponseEntity<?> updateAge(@PathVariable int id, @RequestParam int age) {
if (id < 0) {
throw new ValidationException("Id cannot be less than 0");
}
if (age < 20 || age > 60) {
throw new ValidationException("Age must be between 20 to 60");
}
User user = findUser(id);
user.setAge(age);
return ResponseEntity.accepted().body(user);
}
By default @RestControllerAdvice
is applicable to the whole application but you can restrict it to a specific package, class or annotation.
For package level restriction you can do something like:
@RestControllerAdvice(basePackages = "my.package")
or
@RestControllerAdvice(basePackageClasses = MyController.class)
To apply to specific class:
@RestControllerAdvice(assignableTypes = MyController.class)
To apply it to controllers with certain annotations:
@RestControllerAdvice(annotations = RestController.class)
ResponseEntityExceptionHandler
ResponseEntityExceptionHandler provides some basic handling for Spring exceptions.
We can extend this class and override methods to customize them:
@RestControllerAdvice
public class GlobalResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return errorResponse(HttpStatus.BAD_REQUEST, "Required request params missing");
}
private ResponseEntity<Object> errorResponse(HttpStatus status, String message) {
return ResponseEntity.status(status).body(message);
}
}
To register this class for exception handling we have to annotate it with @ResponseControllerAdvice
.
Again there are many things that can be done here and it depends on your requirements.
Which to Use When?
As you can see, Spring provides us with different options to do exception handling in our apps. You can use one or a combination of them based on your needs. Here is the rule of thumb:
- For custom exceptions where your status code and message are fixed, consider adding
@ResponseStatus
to them. - For exceptions where you need to do some logging, use
@RestControllerAdvice
with@ExceptionHandler
. You also have more controller over your response text here. - For changing the behavior of the default Spring exception responses, you can extend the
ResponseEntityExceptionHandler
class.
Note: Be careful in mixing these options in the same application. If the same thing is handled at more than one places, you might get a different behavior than expected.
Conclusion
In this tutorial, we discussed several ways to implement an exception handling mechanism for a REST API in Spring.
As always, the code for the examples used in this article can be found on Github.