ExceptionHandling in Spring Boot Applications

In this article, we’ll discuss how to properly handle exceptions in Spring Boot applications. We’ll start by looking at the different types of exceptions that can be thrown and the best way to catch them. We’ll then take a look at some specific examples of how to handle exceptions in a Spring Boot application. Finally, we’ll discuss how to log exception information so that you can troubleshoot problems more effectively.

Using @ResponseStatus annotation

Earlier versions of Spring Boot applications used @ResponseStatus annotation to send the exception/error code and messages in the response when ever exception happend.

You can define exception class like below and set the error code and message to be sent in the response when ever

@ResponseStatus(code = HttpStatus.NOT_FOUND,reason = "Department not found" )
public class DeptNotFoundException extends RuntimeException {
    public DeptNotFoundException() {
    }

    public DeptNotFoundException(String message) {
        super(message);
    }

    public DeptNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

In controller class we can throw DeptNotFoundException if we do not find department

@RestController
@RequestMapping("/department")
public class DepartmentController {

    @Autowired
    DepartmentService departmentService;

    @GetMapping(value="/{id}")
    public Department getDepartmentById(@PathVariable Integer id)  {
        Optional<Department> dept = departmentService.getDepartmentById(id);
        return dept.orElseThrow(DeptNotFoundException::new);
    }

   
}

If we try to get Department information which is not existing we will receive error response like below

{
    "timestamp": "2022-04-08T14:00:53.642+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "Department not found",
    "path": "/department/1000"
}

A major drawback of this approach is tight coupling of exception and messages. If we want to throw another exception with different message we have to define another message

We can overcome this problem to some extent by declaring a GenericException without any reason attribute and providing information in the message

@ResponseStatus(code = HttpStatus.NOT_FOUND )
public class GenericException extends RuntimeException {
public GenericException() {
}

public GenericException(String message) {
super(message);
}

public GenericException(String message, Throwable cause) {
super(message, cause);
}
}

@RestController
@RequestMapping("/department")
public class DepartmentController {

    @Autowired
    DepartmentService departmentService;

    @GetMapping(value="/{id}")
    public Department getDepartmentById(@PathVariable Integer id)  {
        Optional<Department> dept = departmentService.getDepartmentById(id);
        return dept.orElseThrow( () -> new GenericException("Department not found"));
      
    }
   
}

With above approach , we can send different message but still we can’t send different Http status code.

Note

If you are using Spring Boot 2.3 and above, you should include following property in application.properties to view the full error/exception

server.error.include-message=always

Using ResponseStatusException class

Spring 5 introduced ResponseStatusException class which simplifies exception/error handling in SpringBoot REST API’s

We can create ResponseStatusException instance with status code .We can optionally provide reason and cause also.

Following are the constructions provided by ResponseStatusException class.

public ResponseStatusException(HttpStatus status)
public ResponseStatusException(HttpStatus status, @Nullable String reason)
public ResponseStatusException(HttpStatus status, @Nullable String reason, @Nullable Throwable cause)

@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    EmployeeService employeeService;

   

    @GetMapping("/{id}")
    public Employee getEmployee(@PathVariable Integer id) {
        return employeeService.getEmployeeById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,"Employee not found with id : "+ id));
    }

Pros

  1. Since it is inbuilt class , error handling can be implemented quiet fast.
  2. There is no need for creating custom exception classes unless there is specific requirement as it provides option for sending reason and cause along with HTTP status code.

Using @ExceptionHandler

We can add methods annotated with @ExceptionHandler to any controller to specifically handle exceptions thrown by Request mapping methods. We can use these methods for send error response using

1. Handle predefined exceptions

2. Build a custom error message

Handle predefined exceptions

The following code demonstrates handling predefined exception.

Let’s see the response received by the rest client if DataIntegrityViolationException occurs in EmployeeController class.

Instead of sending Internal Server error response, Using @ExceptionHandler and @ResponseStatus annotation we can convert a predefined exception to an HTTP Status code

@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    EmployeeService employeeService;    


@ResponseStatus(value=HttpStatus.CONFLICT,
            reason="Data integrity violation")  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConstraintViolated2() {
        // Nothing to do
        System.out.println("in const violation");
    }
}

Build a custom error message

Instead of sending HTTP response code, you can also send error response string or or as an object in the response.

Following code sends error message as string

 @ExceptionHandler(DataIntegrityViolationException.class)
    public String handleDataIntegrityViolationException() {
       return "Database error occurred";
    }

Following code sends error message as custom object.

@ExceptionHandler(DataIntegrityViolationException.class)
    public ErrorResponse handleDataIntegrityViolationException() {
        return new ErrorResponse("Database error occurred",HttpStatus.CONFLICT.value());
    }

Global Exception Handling

Using @ControllerAdvice

@ExceptionHandler allows to you handle exception inside controller, @ControllerAdvice allows you to handle exception across the application.

@ControllerAdvice allows you to handle exceptions in centralized way. As the name indicates the annotated class assists a “Controller”. By default  the @ControllerAdvice annotated class will assist all known Controllers. You can configure it to assist only subset of of Controller classes.

Exception handlers you saw above can be defined on a controller-advice class – but now they apply to exceptions thrown from any controller. Here is a simple example:

@ControllerAdvice
public class GlobalExceptionHandler  {

    @ResponseStatus(value= HttpStatus.CONFLICT,
            reason="Data integrity violation")  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleDataIntegrityViolationException() {
        
    }

}

You can also send custom error response message in response. You have to use @ResponseBody annotation to send the object in response

 @ExceptionHandler(DataIntegrityViolationException.class)
    @ResponseBody
    public ErrorDetails handleDataIntegrityViolationException() {
        ErrorDetails errorDetails = new ErrorDetails(LocalDate.now(),"DataIntegrityViolationException exception", "");
        return errorDetails;
    }

Using @RestControllerAdvice

You can further simplify the ControllerAdvice classes using @RestControllerAdvice annotation.

It is a convenience annotation that is itself annotated with @ControllerAdvice and @ResponseBody.

When using @RestControllerAdvice annotation, you omit the @ResponseBody annotation when sending back custom error response.

 @RestControllerAdvice
public class RestControllerGlobalExceptionHandler extends ResponseEntityExceptionHandler {


    @ExceptionHandler(DataIntegrityViolationException.class)
    public ErrorDetails handleDataIntegrityViolationException() {
        ErrorDetails errorDetails = new ErrorDetails(LocalDate.now(),"DataIntegrityViolationException exception", "");
        return errorDetails;
    }

Using ResponseEntityExceptionHandler class

Spring framework has a ResponseEntityExceptionHandler convenient base class for @ControllerAdvice classes that wish to provide centralized exception handling across all @RequestMapping methods through @ExceptionHandler methods.

This base class provides an @ExceptionHandler method for handling internal Spring MVC exceptions. This method returns a ResponseEntity for writing to the response with a message converter.

This base class provides default implementation for following exceptions.You can override the coreesponding methos and provide your own custom response message to client. This class very useful if you are validating the Request paths and models.

HttpRequestMethodNotSupportedException
HttpMediaTypeNotSupportedException
HttpMediaTypeNotAcceptableException
MissingPathVariableException
MissingServletRequestParameterException
ServletRequestBindingException
ConversionNotSupportedException
TypeMismatchException
HttpMessageNotReadableException
HttpMessageNotWritableException
MethodArgumentNotValidException
MissingServletRequestPartException
NoHandlerFoundException
AsyncRequestTimeoutException

Here is a simple example using ResponseEntityExceptionHandler

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    

    @Override
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(LocalDate.now(),"HttpRequestMethodNotSupported exception", ex.getMessage());
        return new ResponseEntity<>(errorDetails, HttpStatus.METHOD_NOT_ALLOWED);
    }
}

When you invoke a PATCH method with GET option, The application will raise HttpRequestMethodNotSupportedException .

The above code will catch the exception and send the custom response back to client.

What to Use When?

  1. For user defined exceptions consider adding @ResponseStatus to them
  2. For all other exceptions implement an @ExceptionHandler method on a @ControllerAdvice or @RestControllerAdvice class
  3. For Controller specific exception handling, add @ExceptionHandler methods to your controller.

Similar Posts