Pattern for rich error handling in gRPC

Include additional error details in the response Metadata. However, still make sure to provide a useful status code and message. In this case, you can add RegisterUserResponse to the Metadata.

In gRPC Java, that would look like:

Metadata.Key<RegisterUserResponse> REGISTER_USER_RESPONSE_KEY =
    ProtoUtils.keyForProto(RegisterUserResponse.getDefaultInstance());
...
Metadata metadata = new Metadata();
metadata.put(REGISTER_USER_RESPONSE_KEY, registerUserResponse);
responseObserver.onError(
    Status.INVALID_ARGUMENT.withDescription("Email or password malformed")
      .asRuntimeException(metadata));

Another option is to use the google.rpc.Status proto which includes an additional Any for details. Support is coming to each language to handle the type. In Java, it'd look like:

// This is com.google.rpc.Status, not io.grpc.Status
Status status = Status.newBuilder()
    .setCode(Code.INVALID_ARGUMENT.getNumber())
    .setMessage("Email or password malformed")
    .addDetails(Any.pack(registerUserResponse))
    .build();
responseObserver.onError(StatusProto.toStatusRuntimeException(status));

google.rpc.Status is cleaner in some languages as the error details can be passed around as one unit. It also makes it clear what parts of the response are error-related. On-the-wire, it still uses Metadata to pass the additional information.

You may also be interested in error_details.proto which contains some common types of errors.

I discussed this topic during CloudNativeCon. You can check out the slides and linked recording on YouTube.


We have 3 different ways we could handle the errors in gRPC. For example lets assume the gRPC server does not accept values above 20 or below 2.

Option 1: Using gRPC status codes.

   if(number < 2 || number > 20){
        Status status = Status.FAILED_PRECONDITION.withDescription("Not between 2 and 20");
        responseObserver.onError(status.asRuntimeException());
    }

Option 2: Metadata (we can pass objects via metadata)

   if(number < 2 || number > 20){
        Metadata metadata = new Metadata();
        Metadata.Key<ErrorResponse> responseKey = ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance());
        ErrorCode errorCode = number > 20 ? ErrorCode.ABOVE_20 : ErrorCode.BELOW_2;
        ErrorResponse errorResponse = ErrorResponse.newBuilder()
                .setErrorCode(errorCode)
                .setInput(number)
                .build();
        // pass the error object via metadata
        metadata.put(responseKey, errorResponse);
        responseObserver.onError(Status.FAILED_PRECONDITION.asRuntimeException(metadata));
    }

Option 3: Using oneof - we can also use oneof to send error response

oneof response {
    SuccessResponse success_response = 1;
    ErrorResponse error_response = 2;
  }
}

client side:

switch (response.getResponseCase()){
    case SUCCESS_RESPONSE:
        System.out.println("Success Response : " + response.getSuccessResponse().getResult());
        break;
    case ERROR_RESPONSE:
        System.out.println("Error Response : " + response.getErrorResponse().getErrorCode());
        break;
}

Check here for the detailed steps - https://www.vinsguru.com/grpc-error-handling/

Tags:

Grpc