Error Handling for REST API with Spring Boot

Error Handling for REST API with Spring Boot

Problem

A program's readability, reliability, and maintainability can't be complete without proper exception handling. The improper usage may lead to the abuse of exceptions. Spring offers a variety of powerful annotations for exceptions. But before jumping into Spring annotations we will start with plain Java and improve our code.

The most common case of handling exceptions is try and catch.

try {
      // Business logic goes here
} catch (Exception e){
    e.printStackTrace();
}

The problem with above code is that the logic is cut off and flow of program is shut down.

More Spring specific example. Imagine you are handling the login in your authentication service. You first need to find the user by the username and check whether the password matches. (We assume Spring Data JPA is being used).

public LoginResponseDto login(String username, String password) {
    Optional<User> optionalUser = userRepository.findByUsername(username);
    if(optionalUser.isPresent()) {
        if(passwordEnconder.matches(password, optionalUser.get().getPassword())) {
            //Some business logic
        } else {
           //Throw exception or return different response
        }
    } else {
        //Throw exception or return different response
    }
    //Rest of the logic
}

The above code is cumbersome to read. It can handle the exception in two ways: throw an error or return a different response. In both cases there are implications for API design. In the first case, login API forces its clients to use exceptions for ordinary control flow. Calling login(String username, String password) method in your view layer, that is in you controller, will result in try,if successful return normal response and if not, catch and return error response. In the second case returning different responses will force us to change the method signature to accommodate both normal and error responses. Even worse, we have to repeat the same try catchlogic in multiple places. For example every time the resource requested matching to particular id we have to deal with the case whether the requested resource exists or not. We see there is common or repetitive pattern, so there should be a common solution.

Solution

The solution we provide must comply with well-designed API standards. Particularly, we should provide meaningful messages to the clients. Instead of returning whole error stack trace to the client every time there is a request we should provide an error message so that clients themselves can easily understand what went wrong or at least we ourselves (backend guys) should be able trace the error easily.

{
    "message": "Invalid input value",
    "status": 400,
    "errors": [
        {
            "field": "name",
            "value": "",
            "reason": "must not be blank"
        },
        {
            "field": "username",
            "value": "",
            "reason": "must not be blank"
        },
        {
            "field": "roleId",
            "value": "",
            "reason": "must not be null"
        }
    ]
}

The above response is the sample from the validation check for the @RequestBody annotated with@Valid. The response is clear enough for clients to understand what the problem is. (This way frontend guys don't come and keep bothering you saying there is a problem in the server :) )

Another sample response for non-existing resource

{
    "message": "Requested resource doesn't exist",
    "status": 404,
    "errors": []
}

So far we have a very nice JSON response template. Let's create Error Response class for this template.

@Getter
public class ErrorResponse {
    private String message;
    private int status;
    private List<FieldError> errors;
    ...

    @Getter
    public static class FieldError {
        private String field;
        private String value;
        private String reason;

        private FieldError(String field, String value, String reason) {
            this.field = field;
            this.value = value;
            this.reason = reason;
        }
        ...
}

We can manage error objects using ErrorResonse POJO class. We can design a structure that clearly holds responsibility for how to create an ErrorResponse object for a particular exception. Refer to github repository for details of the class.

Next, we need a way to hold error messages in our application. One way is creating exception classes for each type of the exception you have. The problem with this approach is you will end up with tons of exception classes if you have many exception types. A better approach I like is creating enum for error messages.

@Getter
@AllArgsConstructor
public enum ErrorCode {

    RESOURCE_NOT_FOUND("Requested resource doesn't exist", HttpStatus.NOT_FOUND.value()),
    INTERNAL_SERVER_ERROR("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR.value()),

    INVALID_INPUT_VALUE("Invalid input value", HttpStatus.BAD_REQUEST.value()),
    BAD_CREDENTIALS("Username or password is incorrect", HttpStatus.UNAUTHORIZED.value()),

    DUPLICATE_USERNAME("Username is already in use", HttpStatus.CONFLICT.value());

    private final String message;
    private final int status;
}

This approach scales well. We don't have to create a new class every time we have a new exception type but we can simply add a new constructor our ErrorCode and handle all the business logic exceptions in one class.

@Getter
public class GlobalException extends RuntimeException {
    private final ErrorResponse errorResponse;

    public GlobalException(ErrorCode errorCode) {
        this.errorResponse = ErrorResponse.of(errorCode);
    }
}

GlobalException class can handle all the business logic exceptions as long as ErrorCode is present.

So far we defined error codes, exceptions and responses. But how to handle them nicely? How to write consistent, easy to read code? Spring Boot @RestControllerAdvice to handle all exceptions in one place.

@RestControllerAdvice
public class GlobalExceptionHandler {
    private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     *  Handles all the business logic exceptions
     */
    @ExceptionHandler(GlobalException.class)
    public ResponseEntity<ErrorResponse> globalHandler(GlobalException exception) {
        logger.error("GlobalException", exception);
        final ErrorResponse errorResponse = exception.getErrorResponse();
        return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(errorResponse.getStatus()));
    }

    /**
     *  Handles the cases for javax.validation.Valid or @Validated
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
        logger.error("MethodArgumentNotValidException", ex);
        ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, ex.getBindingResult());
        return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(errorResponse.getStatus()));
    }
}

What changed? handleMethodArgumentNotValid handles @Validated exceptions and returns nice JSON response as we presented at the beginning of article. With the globalHandler method we are handling all the business logic exceptions in one place with fewer lines of code. Now we can keep our code clean and consistent by delegating all the exception handling logic to GlobalExceptionHandler class by just providing an error code. Let's try to refactor our login method we saw at the beginning of article.

public LoginResponseDto login(String username, String password) {
        User user = userRepository.findByUsername(username).orElseThrow(
                ()-> new GlobalException(ErrorCode.BAD_CREDENTIALS));
        if(!passwordEncoder.matches(password, user.getPassword())) {
            throw new GlobalException(ErrorCode.BAD_CREDENTIALS);
        }
    //Rest of the logic
}

Another example for duplicate username check

    public void doDuplicateUsernameCheck(String username) {
        if(userRepository.existsByUsername(username)) {
            throw new GlobalException(ErrorCode.DUPLICATE_USERNAME);
        }
    }

Another example to get the user by id

    public User listOne(Long id) {
        User user = userRepository.findById(id).orElseThrow(
                () -> new GlobalException(ErrorCode.RESOURCE_NOT_FOUND));
        return user;
    }

As you can see we got rid of all try and catch lines. In our service classes if there is an exceptional case we are just throwing the exception with the given error code while keeping the consistent code. API clients of service classes, in our case controllers, don't have to worry about the exceptions while forgetting about try and catch and multiple if and elses and since all the exceptions are delegated to GlobalExceptionHandler class.

Conclusion

We learned how to handle exceptions while maintaining a consistent code style. You can refer to the github repository for the whole exception handling logic.