DEV Community

Sebastien Armand
Sebastien Armand

Posted on • Updated on

Go 1.13 errors and gRPC errors

I was trying to figure out if an error was a wrapped grpc error with go 1.13 style errors (currently using the golang.org/x/xerrors package to fill that functionality).

Finding the underlying error will allow me to report it or not depending on the status. A NotFound error doesn't need to appear in our error reporting system.

However you can't access the actual error type for grpc errors since it is not a type the grpc/status package exports.

Basically I want to do:

if grpcCauseError := new(status.statusError); xerrors.As(err, &grpcCauseError) {
    errorCode := status.Code(grpcCauseError)
    switch errorCode {
    case codes.InvalidArgument, codes.NotFound, codes.AlreadyExists, codes.PermissionDenied, codes.Unimplemented, codes.Unauthenticated:
        // Don't report it
    default:
        // Report it
    }
}

Enter fullscreen mode Exit fullscreen mode

This won't work since I cannot access the status.statusError type. I tried making a new grpc error to match against:

if grpcCauseError := grpc.Errorf(codes.Unknown, "error"); xerrors.As(err, &grpcCauseError) {
    errorCode := status.Code(grpcCauseError)
    switch errorCode {
    case codes.InvalidArgument, codes.NotFound, codes.AlreadyExists, codes.PermissionDenied, codes.Unimplemented, codes.Unauthenticated:
        // Don't report it
    default:
        // Report it
    }
}
Enter fullscreen mode Exit fullscreen mode

But that also doesn't work since the grpcCauseError only ends up having the type error and all errors will match that interface so that if statement will always be successful.

The status.statusError type exports 2 methods though: Error() and GRPCStatus(). So we can make an interface that fits this:

type grpcError interface {
    Error() string
    GRPCStatus() *status.Status
}
Enter fullscreen mode Exit fullscreen mode

To be able to use it with xerrors.As() though, we'll need more than a pointer to an interface, we'll need an actual instance of that interface. This can be achieved by either converting an existing grpc error or creating a new type that matches that interface:

baseGrpcError := grpc.Errorf(codes.Unknown, "oops")
grpcCauseError := e.(grpcError)
if xerrors.As(err, &grpcCauseError) {
    errorCode := status.Code(grpcCauseError)
    switch errorCode {
    case codes.InvalidArgument, codes.NotFound, codes.AlreadyExists, codes.PermissionDenied, codes.Unimplemented, codes.Unauthenticated:
        // Don't report it
    default:
        // Report it
    }
}
// OR
type mockGRPCError struct{}

func (*mockGRPCError) Error() string              { return "" }
func (*mockGRPCError) GRPCStatus() *status.Status { return &status.Status{} }

var grpcCauseError grpcError
grpcCauseError = mockGRPCError{}
if xerrors.As(err, &grpcCauseError) {
    errorCode := status.Code(grpcCauseError)
    switch errorCode {
    case codes.InvalidArgument, codes.NotFound, codes.AlreadyExists, codes.PermissionDenied, codes.Unimplemented, codes.Unauthenticated:
        // Don't report it
    default:
        // Report it
    }
}
Enter fullscreen mode Exit fullscreen mode

In both cases, getting the base error can be abstracted in a function so that we get a bit cleaner code:

if grpcCauseError := getGrpcError(); xerrors.As(err, &grpcCauseError) {
    errorCode := status.Code(grpcCauseError)
    switch errorCode {
    case codes.InvalidArgument, codes.NotFound, codes.AlreadyExists, codes.PermissionDenied, codes.Unimplemented, codes.Unauthenticated:
        // Don't report it
    default:
        // Report it
    }
}

func getGrpcError() *grpcError {
    baseGrpcError := grpc.Errorf(codes.Unknown, "oops")
    grpcCauseError := e.(grpcError)
    return grpcCauseError
}
Enter fullscreen mode Exit fullscreen mode

Discussion (0)