In the previous blog I introduced the gRPC calls and told about the absence of error handling in the model itself. Let’s investigate how this came about.

Explicit error feedback

At first I started out modelling my service with explicit resultcodes and reasons as that looked like it should be done. With some best practices the model looked something like this (Request messages and ContractId message omitted):

service ContractService {
    rpc newQuote (NewQuoteRequest) returns (NewQuoteResponse);
}

message NewQuoteResponse {
    Result status = 1;
    string reason = 2;
    ContractId quoteId = 3;
}

enum Result {
    UNKNOWN = 0;(1)
    OK = 1;
    FAILED = 2;
    NOTFOUND = 3;
}
1 the default for an enum field if it is not passed through or has an unknown value is always 0, this way that situation is distinquishable in code.

Biggest change is the Result enum and its inclusion in the NewQuoteResponse message. It looked good to me but I soon found the first problem, although it was more of an annoyance at the model level. Adding the promoteQuote call gives the following:

Listing 1. grpc_contract_api_v1.proto (partial)
service ContractService {
    rpc newQuote (NewQuoteRequest) returns (NewQuoteResponse);
   rpc promoteQuote (ContractId) returns (PromoteQuoteResponse); (1)
 }

message NewQuoteResponse {
    Result status = 1;
    string reason = 2;
    ContractId quoteId = 3;
}

message PromoteQuoteResponse {
    Result status = 1;
    string reason = 2;
}

enum Result {
    UNKNOWN = 0;
    OK = 1;
    FAILED = 2;
    NOTFOUND = 3;
}
1 I hadn’t absorbed the 'design for extensibility' philosophy of gRPC here, hence the use of ContractId as a returntype

As you can see, the status and reason fields are duplicated. With more and more calls, these get duplicated over and over again. As far as I could find Protobuf does not allow for any kind of inheritance to keep the Response messages more clean.

Not yet a big problem in this model, although it is a lot of repetition, but it sure makes for ugly implementing code. That is because in the implementing code it is not possible to handle these field in a generic way; as well, these fields are not generic.

One of the biggest problems I had with these fields was that the Result and Reason fields started messing with the code separation within the service. I had to drag them everywhere and had to create specific return objects to transport them between layers. Lots of same-looking plumbing that made the code fuzzy and difficult to understand at a glance.

Full implementation with code of the service with these fields is here: Reasons branch.

Code with reasons

If we look at the code needed to handle error situations in the newQuote call in our example service when we use Result and Reason fields, it looks a little intimidating with all the plumbing. These next code pieces are from the ContractServiceApiHandler class that is responsible for translations and conditions checking before and after calling the real business code.

public class ContractServiceApiHandler {
    private NewQuoteResponse newQuote(NewQuoteRequest request) {
        String reason = <determine preconditions>;

        if (reason.length() > 0) {
            return NewQuoteResponse.newBuilder().setStatus(Result.FAILED).setReason(reason).build();
        }

        String id = <process request>

        return NewQuoteResponse.newBuilder()
                .setQuoteId(ContractId.newBuilder().setId(id).build())
                .setStatus(Result.OK)
                .setReason("Quote.added")
                .build();
    }
}

The same with the clean definition (without Result and Reason fields) as introduced in the previous blog we get the following code. Much cleaner and more readable from a business standpoint.

public class ContractServiceApiHandler {
    public NewQuoteResponse newQuote(NewQuoteRequest request) {
        String reason = <determine preconditions>;
        if (reason.length() > 0) {
            throw new PreConditionNotMetException(reason);
        }

        String id = <process request>

        return NewQuoteResponse.newBuilder().setContractId(id).build();
    }
}

The decision to use exceptions for non-happy flow is a very practical one, mainly because the handling of errors towards gRPC now becomes something that can be generalized. The gRPC API handling class that calls above code, note that handling both the normal response and the error response is a single call:

public class ContractServiceApi extends ContractServiceGrpc.ContractServiceImplBase {
    private static final ContractServiceApiHandler handler = new ContractServiceApiHandler();

    @Override
    public void newQuote(NewQuoteRequest request, StreamObserver<NewQuoteResponse> responseObserver) {
        try {
            returnResponse(responseObserver, handler.newQuote(request)); (1)
        } catch (Exception e) {
            returnError(responseObserver, e); (2)
        }
    }
}
1 Normal handling returns the response to the gRPC responseObserver.
2 In case of an exception, Convert the exception and return an error to the gRPC responseObserver.

The generic error handling based on thrown exceptions is write-once code which means adding more RPC calls remains clean and simple. See the 2 return methods below.

    private <T> void returnResponse(StreamObserver<T> responseObserver, T response) {
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }

    private void returnError(StreamObserver<?> responseObserver, Throwable exc) {
        if (exc instanceof PreConditionNotMetException) {
            var e = (PreConditionNotMetException) exc;
            var status = Status.FAILED_PRECONDITION.withCause(exc).withDescription(e.getConditionFailuresAsString());
            responseObserver.onError(status.asRuntimeException());
        }
        if (exc instanceof NotFoundException) {
            responseObserver.onError(Status.NOT_FOUND.asRuntimeException());
        }
        var status = Status.fromThrowable(exc);
        responseObserver.onError(status.asRuntimeException());
    }

As you can see, the returnError(..) method works by adding creating a Status object that has the problem that occurred coded as a predefined Status code. The io.grpc.Status class is part of the gRPC API library and has lots of useful predefined constants like Status.OK, Status.NOT_FOUND etcetera, 16 in total. These codes are the only status codes Google uses in its API’s. [1]

For me this is fine-grained enough because I encoded errors in the Description field, if more control is needed other solutions might come in view, like adding Metadata to the returned Status.

Final words

These two basic ways of returning the result of a call are very distinct and choosing one over the other is a major architectural decision.

for more on this, Google explains the Google Cloud API rules they use in fine details, very good reading to get up to speed on this subject.

Full implementation of the clean version is here: ErrorFlow branch.

More to come later…​.

shadow-left