Consistent and Effective Approach to Handle Exceptions

editor
Selling Cars
Est. Reading Time:
5 mins

Java provides three kinds of throwables: checked exceptions, runtime exceptions, and errors. There is some confusion among programmers as to when it is appropriate to use each kind of throwable. While the decision is not always clear-cut, there are some general rules that provide strong guidance.

The rule in deciding whether to use a checked or an unchecked exception is as follows:

Checked: Use checked exceptions for conditions from which the caller can reasonably be expected to recover.
Unchecked: The great majority of runtime exceptions indicate precondition violations.
Errors: Errors are negative exit conditions meant to shut down the application.

As per Effective Java (3rd Edition)

Many Java programmers dislike checked exceptions, but used properly, they can improve APIs and programs. Unlike return codes and unchecked exceptions, they force programmers to deal with problems, enhancing reliability. That said, overuse of checked exceptions in APIs can make them far less pleasant to use. If a method throws checked exceptions, the code that invokes it must handle them in one or more catch blocks, or declare that it throws them and let them propagate outward. Either way, it places a burden on the user of the API. The burden increased in Java 8, as methods throwing checked exceptions can’t be used directly in streams.

So, there is a very thin line between deciding whether an exception should be checked or unchecked. The more checked exceptions, the more you have to deal with complexity. Sometimes code becomes very unreadable when there are a lot of checked exceptions while you are throwing or you have to catch even if you are not intended to recover but just to convert the exception in a user-readable response code.

As per Effective Java (3rd Edition)

In fact, the exception-based idiom is far slower than the standard one. On my machine, the exception-based idiom is about twice as slow as the standard one for arrays of one hundred elements.

So, the question remains how it should be handled elegantly. Instead of obeying the best practices which are quite confusing, we need to define some defacto standard to deal with Exception handling.

Rule 1

Favour the use of standard exceptions wherever applicable.

ExceptionOccasion for Use
IllegalArgumentExceptionThe non-null parameter value is inappropriate
IllegalStateExceptionObject state is inappropriate for method invocation
NullPointerExceptionThe parameter value is null where prohibited Index parameter
IndexOutOfBoundsExceptionValue is out of range
ConcurrentModificationExceptionConcurrent modification of an object has been detected where it is prohibited
UnsupportedOperationExceptionThe object does not support the method
IOExceptionError occurred during I/O Operation

But again standard exceptions are good candidates for framework or some utility programs. But when it comes to business/application category exceptions then use rule 2.

Rule 2

Create only one application-level global runtime exception, say “ApplicationException” and initialize with type-safe enum based response code.

Now the question is how to make the flow recoverable in case of the exceptional condition. This is where enum based response code will serve the purpose. An application is never intended to recover from all exceptional conditions and few of the business-specific conditions are candidates of recovery and these conditions are well defined during the requirement and design phase. Hence, we already know in advance which flow we want to recover. I will clear it further in the examples.

Create the below interface to define the application codes.

public interface ApplicationCode {
   String code();
   String message();
}

Define the all required response codes implementing ApplicationCode in the application.

public enum ErrorCode implements ApplicationCode {
   NOT_ALLOWED ("BK0405", "Action is not allowed"),
   VEHICLE_NOT_FOUND ("BKV404", "Appointment not found"),
   AUCTION_NOT_FOUND ("BKA404", "Auction not found"),

   BID_REJECTED ("BKA416", "Bid is rejected"),
   BID_REJECTED_STEP_RATE ("BKA4161", "Doesn't meet minimum step rate criteria"),
   BID_REJECTED_LOWER_PRICE ("BKA4162", "Bid is placed with lower price"),
   BID_REJECTED_OVERFLOW_MAX_CAP ("BKA4163", "Exceeded the max price of the auction. Place bid with lower price"),

   AUCTION_ALREADY_RUNNING("BKA409", "Auction already running"),
   BAD_AUCTION_REQUEST("BKA400", "Bad request");

  private String code;
  private String message;

  ErrorCode(String code, String message) {
       this.code = code;
       this.message = message;
   }

   @Override
   public String code() {
       return code;
   }

   @Override
   public String message() {
       return message;
   }

}

Define the Global Application Exception

public class ApplicationException extends RuntimeException {

    private ApplicationCode applicationCode;
    private String description;
    private List<ApplicationException> nestedExceptions = new ArrayList<>(  );

    public ApplicationException() {
        super( "Internal Server Error" );
    }

    public ApplicationException(String message) {
        super( message );
        applicationCode = new ApplicationCodeImpl( "500", message );
    }

    public ApplicationException(ApplicationCode applicationCode) {
        super( applicationCode.message() );
        this.applicationCode = applicationCode;
    }

    public ApplicationException(ApplicationCode applicationCode, String description) {
        this( applicationCode );
        this.description = description;
    }

    public ApplicationException(String errorCode, String message, String description) {
        this( new ApplicationCodeImpl( errorCode, message ), description );
    }

    public ApplicationException(ApplicationCode applicationCode, Throwable cause) {
        super( cause );
        this.applicationCode = applicationCode;
    }

    public ApplicationException(String message, Throwable cause) {
        this( new ApplicationCodeImpl( "500", message ), cause );
    }

    public void addNestedException(ApplicationException e) {
        nestedExceptions.add( e );
    }

    public List<ApplicationException> getNestedExceptions() {
        return nestedExceptions;
    }

    public ApplicationCode getApplicationCode() {
        return applicationCode;
    }

    public String getDescription() {
        return description;
    }

    static class ApplicationCodeImpl implements ApplicationCode {

        private String code;
        private String message;

        public ApplicationCodeImpl(String code, String message) {
            this.code = code;
            this.message = message;
        }

        @Override
        public String code() {
            return code;
        }

        @Override
        public String message() {
            return message;
        }

    }

    public static class Builder {

        private ApplicationException exception;
        private List<ApplicationException> nested = new ArrayList<>(  );

        private Builder() {
        }

        public static Builder builder() {
            return new Builder();
        }

        public Builder root(ApplicationCode code) {
            return this.root( code, null );
        }

        public Builder root(ApplicationCode code, String descr) {
            this.exception = new ApplicationException( code, descr );
            return this;
        }

        public Builder error(ApplicationCode code) {
            return this.error( code, null );
        }

        public Builder error(ApplicationCode code, String descr) {
            this.nested.add( new ApplicationException( code, descr ) );
            return this;
        }

        public boolean isErrorExist() {
            return exception != null || nested.size() > 0;
        }

        public Optional<ApplicationException> build() {
            if (!isErrorExist()) return Optional.empty();
            if (nested.size() == 0) return Optional.ofNullable(exception);
            if (exception == null && nested.size() == 1) return Optional.of(nested.iterator().next());
            if (exception == null) {
                StringBuilder stringBuilder = new StringBuilder();
                nested.stream().forEach(n -> stringBuilder.append(n.getMessage()).append(" : "));
                this.exception = new ApplicationException(stringBuilder.toString());
            }
            this.exception.nestedExceptions = nested;
            return Optional.of(exception);
        }
    }
}

How to use it

Below is one sample example to use it further.

if (bid.getStatus() == Bid.Status.REJECTED) {
   throw new ApplicationException( ResponseCode.BID_REJECTED );
}

And, if you want to recover from this exception then you can do it in the following way.

try {
   biddingProcessor.placeBid( user.getId(), request );
} catch (ApplicationException e) {
   if (e.getApplicationCode() == ResponseCode.BID_REJECTED) {
       doSomethingToRecover();
   }
}

Advantage of this approach:

It also benefits you to define multiple error conditions, especially while validating the data. See the example below:

Create the Error Builder

ApplicationException.Builder errorBuilder = ApplicationException.Builder.builder( );

And then validate the request parameters and add them into the builder.

private void validate(ApplicationException.Builder builder, PlaceBidRequest request, Auction auction) {
   int hbv = auction.getHbv() <= 0 ? auction.getAnchorBid() : auction.getHbv();
   if (request.getAmount() - hbv < auctionRules.getStepRate()) {
       builder.error( ResponseCode.BID_REJECTED_STEP_RATE );
   }

   if (request.getAmount() - hbv <= auction.getHbv()) {
       builder.error( ResponseCode.BID_REJECTED_LOWER_PRICE );
   }
   if (request.getAmount() < auction.getTargetPrice() * auctionRules.getMaxAllowedPercentage() / 100) {
       builder.error( ResponseCode.BID_REJECTED_OVERFLOW_MAX_CAP );
   }
}

Further, you can build with root exception and throw:

if (errorBuilder.isErrorExist()) {
   throw errorBuilder.root( ResponseCode.BID_REJECTED ).build().get();
}

Now the final step where you want to send the same response over HTTP. In the case of spring controllers, we can create an interceptor to deal with ApplicationException and send the desired response in the following way.

@ControllerAdvice
@Slf4j
public class AppControllerAdvice {

    @Autowired HttpStatusMapping mapping;

    @ExceptionHandler({ApplicationException.class})
    public ResponseEntity<Reason> handleException(Exception exp) {
        Reason reason = Reason.from(exp);
        HttpStatus mappedStatus = HttpStatus.resolve(exp.getApplicationCode().code());
        return ResponseEntity.status(mappedStatus).body(reason);
    }

    static class Reason {
            private String code;
            private String message;

            private List<Reason> nested = new ArrayList<>();

            public static Reason from(final ApplicationException e) {
                final Reason reason = toReason(e);
                e.getNestedExceptions().stream().forEach(n -> reason.getNested().add(toReason(n)));
                   return reason;
            }
    }
}

Above code will generate the following type of response

{
    "code": "BKA416",
    "message": "Bid is rejected",
    "nested": [
      {
        "code": "BKA4161",
        "message": "Doesn't meet minimum step rate criteria"
      },
      {
        "code": "BKA4161",
        "message": "Bid is placed with lower price"
      }
    ]
}

The mapping of application-specific codes can be defined in some mapping file. I used YAML format and created a reader class HttpStatusMapping to read the status code against the application code. Here is the sample structure for YAML.

response-mapping:
 - application-code: BK0405
   http: 405
   message: Action is not allowed

 - application-code: BKV404
   http: 404
   message: Appointment not found

 - application-code: BKA404
   http: 404
   message: Auction not found

 - application-code: BKA416
   http: 416
   message: Bid with higher price already applied

 - application-code: BKA409
   http: 409
   message: Auction already running

 - application-code: BKA400
   http: 400
   message: Bad Request

All set.

References:

With words from Deepak Chauhan, Director Of Engineering at CARS24. If you wish to be a part of his journey, connect with him on LinkedIn.

Sell Car in One Visit

Start Car Evaluation