Skip to content

Exception handling

Web applications usually consist of three basic layers:

  • controllers
  • service layer
  • the persistence layer, i.e. the layer responsible for saving and reading data from the database

When the end user uses such an application (by using the [REST] (rest.md) API or interacting with the HTML view), an error may occur (i.e. [exception] (../javaAdvanced/exceptions.md) is thrown) on one of these layers. In such cases, the programmer should handle the error in some way. Since exceptions can be of different [type] (../javaAdvanced/exceptions.md#exception-types), we don't always know exactly which instance of the exception will be thrown. The so-called exception handlers "take control" when an error occurs.

Annotation @ExceptionHandler

In order to handle the error of a specific type (a subgroup of exception types) we use the annotation @ExceptionHandler. This annotation can be used over a method that handles exceptions. As the value field of this annotation we specify the class (or class list) of the exception that this method will handle. This method can be argumentless or optionally take an exception instance.

We should treat the method on which we place @ExceptionHandler as a normalendpoint, i.e .:

  • in the case when it is used to handle errors with the REST API, it can return an object directly that will go to the body of the response (in the case of using an annotation @ResponseBody or @RestController above the class) or an object ResponseEntity
  • we can set the returned status with the @ResponseStatus annotation

Local @ExceptionHandler

Exception handler can be defined inside a class that is a controller. In this case, it only handles exceptions that occurred as a result of calling one of the endpoints defined inside this class, e.g .:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Error {
  private String message;
}
public class SdaException extends RuntimeException {
  public SdaException(final String message) {
    super(message);
  }
}
public class SpecificSdaException extends SdaException {
  public SpecificSdaException(final String message) {
    super(message);
  }
}
public class VerySpecificSdaException extends SpecificSdaException {
  public VerySpecificSdaException(final String message) {
    super(message);
  }
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class ControllerWithLocalExpHandler {

  @GetMapping("/api/resource")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void willAlwaysThrow() {
    throw new SpecificSdaException("Boom something bad happened in the controller method");
  }

  @GetMapping("/api/resource/subresource")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void alsoAlwaysThrows() {
    throw new VerySpecificSdaException("Boom a corner case occurred");
  }

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(SpecificSdaException.class)
  public Error handleSpecificSdaException(final SdaException exception) {
    log.debug("something bad has happened...");
    return new Error(exception.getMessage());
  }

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(VerySpecificSdaException.class)
  public Error handleVerySpecificSdaException(final SdaException exception) {
    log.debug("something bad has happened...");
    return new Error(exception.getMessage());
  }
}

In the example above, after sending the request to / api / resource, we will get the answer:

{"message":"Boom something bad happened in the controller method"}

This happened because of the two available exception handlers, only one of them is able to handle the exception of the SpecificSdaException type. Note that the exception instance can be an argument to the exception handling method (which can, like a regular endpoint, have any name). Also, the type of the argument does not necessarily have to be the same as the type supported by the handler. It can be of any type from which the "caught" class inherits (i.e. it could also be either a Throwable or Exception, for example).

When calling the api/resource/subresource endpoint, the response will be:

{"message":"Boom a corner case occurred"}

In this case, both exception handlers are able to handle the exception thrown , but the one that handles the type VerySpecificSdaException is more specific to the thrown exception. Therefore, we very often define an exception handler that handles Exception. Most often it handles those exceptions in a generic way that we didn't think could be thrown.

Global ExceptionHandler - @ControllerAdvice

When an application consists of many resources, where the access has been divided into many controllers, often when we try to invoke them, there may be exceptions of the same type, which we want to handle the same way. Instead of defining the same exception handlers inside each controller, it is better to create one that supports all (or some) controllers. We can create this exception handler in a separate class. Such a class should be marked with the annotation @ControllerAdvice. If you want to handle errors with the [REST] (rest.md) API, additionally connect it with the annotation @ResponseBody or use the annotation @RestControllerAdvice, which is a combination of both. Inside the class, we define, with the @ExceptionHandler annotation, methods to handle exceptions of a given type.

The example below shows two controllers, errors of which are handled by one exception handler defined in a separate file:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Error {
  private String message;
}
public class SdaException extends RuntimeException {
  public SdaException(final String message) {
    super(message);
  }
}
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FirstController {
  @GetMapping("/api/resource")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void willAlwaysThrow() {
    throw new SdaException("Boom something bad happened in the controller method");
  }
}
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SecondController {
  @GetMapping("/api/resource2")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void alsoAlwaysThrows() {
    throw new SdaException("Boom a corner case occurred");
  }
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
@ResponseBody // or @RestControllerAdvice
@Slf4j
public class GlobalErrorHandler {

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(SdaException.class)
  public Error handleSdaException(final SdaException exception) {
    log.debug("something bad has happened...");
    return new Error(exception.getMessage());
  }
}

If a request is made on both /api/resource and /api/resource2, the exceptions that occur will be handled by the handleSdaException method in theGlobalErrorHandler class.

NOTE: When using @ControllerAdvice or @RestControllerAdvice, we do not need to additionally mark the class with the annotation @Component. This annotation is located "inside" the @ControllerAdvice annotation.