Go 1.13 errors and gRPC errors
Tags: go, errors
Published: 2019-08-28 20:47:11

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:

```go
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
	}
}

```

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

```go
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
	}
}
```
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:

```go
type grpcError interface {
	Error() string
	GRPCStatus() *status.Status
}
```
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:

```go
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
	}
}
```
In both cases, getting the base error can be abstracted in a function so that we get a bit cleaner code:
```go
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
}
```