throw custom exception with Spring Data Rest

I’m developing a backend with Spring Boot and Spring Data Rest. I need to throw a custom exception instead of the Spring Data Rest exceptions that have a different structure.

This is a code that I wrote but it doesn’t throw my exception but a generic RuntimeException

import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.Locale;

@ControllerAdvice(annotations = RepositoryRestController.class)
public class RestRepositoryExceptionHandler {

    @ExceptionHandler({DataIntegrityViolationException.class})
    public RuntimeException handle(Exception e, Locale locale) {
        throw new RuntimeException("REST_REPOSITORY_EXCEPTION", e);
//        throw new RuntimeException("DATA_ALREADY_EXISTS");
    }

}

and this is a repository

import it.edistribuzione.smartgrid.model.User;
import lombok.NonNull;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import java.util.Optional;

@RepositoryRestResource(path = "users")
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);

    Boolean existsByUsername(String username);

    boolean existsById(@NonNull Long id);
}

Answer

Normally your exception handler is the last layer of exception handling that you write as a developer. You normaly don’t re-throw an exception because then it will be handed to default exception handling mechanism of the server that hosts your application.

You normally return a response with the error that you want the user to see.

Also throwing a RunntimeException will be matched in exception handling mechanism by almost all servers that I know as a 500 error. So the user will see a 500 error which seems like your backend failed and something is messed up.

If you can predict what can go wrong then don’t let the user see a 500 error code but instead inform him for what is bad. In that case when you have received DataIntegrityViolationException it means that in one way or another the input that the user provided is wrong. So it can not be the backend that failed (500 error) but the user that had done a bad request (400 error).

It can also be a safety issue when the client can make the backend fail, and retrieve the stack trace from the default handling mechanism and know information about how the database is built.

Here is a custom exception that you can use in your application

public class ApplicationException extends RuntimeException  {

    private CustomError customError;

    public ApplicationException(CustomError customError){
        super();
        this.customError = customError;
    }
}

And here is an error model class

@Getter
@Setter
@NoArgsConstructor
public class CustomError {

    private int code;
    private String message;
    private String cause;

    public CustomError(int code, String message, String cause) {
        this.code = code;
        this.message = message;
        this.cause = cause;
    }
 }

And this is how your error handling method could be used to return a response that the user understands.

@ControllerAdvice
public class RestRepositoryExceptionHandler {
    
    @ExceptionHandler(ApplicationException.class)
    public ResponseEntity handleApplicationException(ApplicationException e) {
        return ResponseEntity.status(e.getCustomError().getCode()).body(e.getCustomError());
    }


   @ExceptionHandler({DataIntegrityViolationException.class})
    public ResponseEntity handle(Exception e, Locale locale) {
        return ResponseEntity.status(400).body(new CustomError(400, e.getMessage(), "this is the cause that the user will see"));
  }
}

The following line could be reused anywhere in your application like (Controllers, Services, Daos,…)

throw new ApplicationException( new CustomError(400, "This is the error message that the user will see", "this is the cause that the user will see"));

You can of course modify the error code and not throw only 400, and also modify the errorMessage and errorCause in CustomError before throwing the ApplicationException