Overview

In a recent article, I showed you how to add some basic error handling to your Spring Boot application. The approach I showed will work, but it tends to clutter up your controller with error handling, which is not business logic. We can achieve better separation of concerns (and, therefore, more readable and maintainable code), if we move our error handling code outside the controller.

You can start with the code from the previous article, which is found here.

Custom Exceptions

Let's create a custom exception to specifically handle missing arguments. This exception will inherit from Java's Exception class, as shown below.

public class MissingArgumentsException extends Exception {
    public MissingArgumentsException(String errorMessage) {
        super(errorMessage);
    }
    public MissingArgumentsException(String errorMessage, Throwable err) {
        super(errorMessage, err);
    }
}

Next, we will add code in our service's AddNumbers method to check for missing arguments and throw the custom exception, if an argument is missing.

@Qualifier("MathServiceImpl")
@Service
public class MathServiceImpl implements MathService {

    ...

    @Override
    public Integer AddNumbers(Integer firstNumber, Integer secondNumber) throws MissingArgumentsException {
        logger.info("MathServiceImpl.AddNumbers() called with " + firstNumber + " and " + secondNumber);
        if (firstNumber==null || secondNumber==null) {
            String message = "Missing input";
            throw new MissingArgumentsException(message);
        }
        Integer sum = firstNumber + secondNumber;
        return sum;
    }

    ...
}

Java has a rule that you must add a throws clause to a method declaration before we can throw an exception within the method. Hence, in order to add the line:

throw new MissingArgumentsException(message);

we must change the method declaration to:

public Integer AddNumbers(Integer firstNumber, Integer secondNumber) throws MissingArgumentsException {

Because our class inherits from an interface (MathService), we must also modify the method declaration within the interface and within any other classes that implement that interface.

public interface MathService {
    Integer AddNumbers(Integer firstNumber, Integer secondNumber) throws MissingArgumentsException;
    ...
}

Finally, we modify our controller method, wrapping the call to the service in a try/catch structure and returning an error HTTP code and error message only if the exception is caught in the catch block, as shown below:

@PostMapping(path="AddNumbers", consumes = "application/json", produces = "application/json")
public ResponseEntity<AddNumbersOutput> AddNumbersPost(@RequestBody AddNumbersInput input) throws MissingArgumentsException {
    Integer firstNumber = input.getFirstNumber();
    Integer secondNumber = input.getSecondNumber();
    String personName = input.getPersonName();

    Integer sum = null;
    try {
        sum = mathService.AddNumbers(firstNumber, secondNumber);
    } catch (Exception e) {
        String message = e.getMessage();
        AddNumbersOutput output = new AddNumbersOutput(null, message);
        return new ResponseEntity<AddNumbersOutput>(output, HttpStatus.BAD_REQUEST);
    }

    ...

    String message = "The sum of " + firstNumber + " and " + secondNumber 
        + " is " + sum + ",  " + personName;
    AddNumbersOutput output = new AddNumbersOutput(sum, message);
    return new ResponseEntity<AddNumbersOutput>(output, HttpStatus.OK);
}

Now, if we call the Controller method, using a tool like Postman, and try to omit one of the required arguments, the controller will return an HTTP 400 (BAD REQUEST) and an error message in the body, as shown in Fig. 1

Bad HTTP request and response - missing input data

Conclusion

The behavior does not change much from our implementation in the earlier article, but structuring our code like this makes it cleaner by keeping error handling out of the controller. This would be more apparent if we had a controller that called a series of methods, some of which were nested multiple levels deep. We could throw exceptions in the called methods and only catch them in the controller, which sits at the top of the stack. Exceptions would bubble up and be handled in one place.

You can find the code for this article here.