Exception Handling in Spring

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.

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.