Overview

In a recent article, I showed you how to errors in a Java Spring Boot application. You can start with the code from that article, which is found here.

This article will describe a more elegant way of handling exceptions.

The Problem with Try/Catch

Here is the relevant error-handling code from the controller's DivideNumbers method, described in the previous article:

@PostMapping(path="DivideNumbers", consumes = "application/json", produces = "application/json")
public ResponseEntity<DivideNumbersOutput> DivideNumbersPost(@RequestBody DivideNumbersInput input) throws MissingArgumentsException {
    Integer firstNumber = input.getFirstNumber();
    Integer secondNumber = input.getSecondNumber();
    String personName = input.getPersonName();
    Integer quotient = null;
    try {
        quotient = mathService.DivideNumbers(firstNumber, secondNumber);
    } catch (ArithmeticException e) {
        String message = "An error has occurred, " + personName + ": " + e.getMessage();
        DivideNumbersOutput output = new DivideNumbersOutput(null, message);
        return new ResponseEntity<DivideNumbersOutput>(output, HttpStatus.BAD_REQUEST);
    } catch (Exception e) {
        String message = "An error has occurred, " + personName + ": " + e.getMessage();
        DivideNumbersOutput output = new DivideNumbersOutput(null, message);
        return new ResponseEntity<DivideNumbersOutput>(output, HttpStatus.BAD_REQUEST);
    }
    String message = firstNumber + " divided by " + secondNumber 
        + " is " + quotient + ", " + personName;
    DivideNumbersOutput output = new DivideNumbersOutput(quotient, message);
    return new ResponseEntity<DivideNumbersOutput>(output, HttpStatus.OK);
}

There is nothing wrong with this code. It handles errors gracefully. However, it can become inefficient if we have the same error handling in many methods of our application. In that case, we will end up copying and pasting the same code in multiple places, and we will need to maintain the same code in multiple places.

The Spring framework provides a built-in way to address this concern using the @ControllerAdvice annotation, which allows you to handle exceptions across all modules of an application.

We will make a slight change to our application, adding MathInput and MathOutput model classes, These are similar to the input and output classes for adding and dividing numbers, in order to provide generic inputs and outputs for our math methods.

public class MathInput {
    private Integer firstNumber;
    private Integer secondNumber;
    private String personName;

    public Integer getFirstNumber() {
        return firstNumber;
    }
    public void setFirstNumber(Integer firstNumber) {
        this.firstNumber = firstNumber;
    }

    public Integer getSecondNumber() {
        return secondNumber;
    }
    public void setSecondNumber(Integer secondNumber) {
        this.secondNumber = secondNumber;
    }

    public String getPersonName() {
        return personName;
    }
    public void setPersonName(String personName) {
        this.personName = personName;
    }
}
public class MathOutput {
    private Integer result;
    private String message;

    public Integer getQuotient() {
        return result;
    }
    public void setQuotient(Integer result) {
        this.result = result;
    }

    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }

    public MathOutput(Integer result, String message) {
        super();
        this.result = result;
        this.message = message;
    }
}

Next, we add a class to hold a ControllerAdvice class that will contain all the error handling for our application. I named this class DgTestControllerAdvice and decorated it with @ControllerAdvice to indicate it is a ControllerAdvice class.

@ControllerAdvice
public class DgTestControllerAdvice {

I added two methods to this class: One to handle errors of type ArithmeticException and one to handle all other exceptions. Each class is decorated with three annotations:

  • @ExceptionHandler specifies the exception that will trigger calling this method
  • @ResponseStatus specifies the HTTP Response Code to return to the client. This frees us from having to create a ResponseEntity class
  • @ResponseBody tells Spring to serialize the body of the response as JSON

Here are the two methods:

@ExceptionHandler(ArithmeticException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
MathOutput handleArithmeticException(ArithmeticException ex) {
    String message = "Math error: " + ex.getMessage();
    MathOutput mathOutput = new MathOutput(null, message);
    return mathOutput;
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
String handleException(Exception ex) {
    String message = "Error: " + ex.getMessage();
    return message;
}

Of course, we can add more logic to these methods (log the error, for example), but I kept it simple to demonstrate how the pattern works.

Now that we have a @ControllerAdvice class with exception handler methods, we no longer need to clutter our controller with error handling code. The refactored DivideNumbersPost method becomes much simpler and the business logic is more clear without the try/catch structure.

    @PostMapping(path="DivideNumbers", consumes = "application/json", produces = "application/json")
    public ResponseEntity<DivideNumbersOutput> DivideNumbersPost(@RequestBody DivideNumbersInput input) throws MissingArgumentsException {
        Integer firstNumber = input.getFirstNumber();
        Integer secondNumber = input.getSecondNumber();
        String personName = input.getPersonName();
        Integer quotient = mathService.DivideNumbers(firstNumber, secondNumber);
        String message = firstNumber + " divided by " + secondNumber 
            + " is " + quotient + ", " + personName;
        DivideNumbersOutput output = new DivideNumbersOutput(quotient, message);
        return new ResponseEntity<DivideNumbersOutput>(output, HttpStatus.OK);
    }

Error Handling Calling DivideNumbers With Controller Advice
Fig. 1 shows the results of running this application and calling the DivideNumbers method using Postman.

Fig. 1

Conclusion

Adding a ControllerAdvice class to your application and moving error handling logic into that class can simplify your code, make error handling reusable and easier to maintain, and improve the readability of your business logic. This article walked you through how to implement this pattern. You can find the code here.