How to best handle errors in Go is a divisive issue, leading to opinion pieces by illustrious bloggers such as Dave Cheney, the commander himself Rob Pike as well as the official Go blog. I’m not going to tackle those opinions here, instead I’m going to talk about best practices for errors when using gRPC and Go.
The gRPC Status package
The Go gRPC implementation has a
status package
which exposes a nice simple interface for creating rich gRPC errors.
For example, lets say you have a method that takes an ID as a parameter,
but the requested ID did not exist in your store. You could just return
the error your store backend returned, but a good gRPC server should
make use of the gRPC error codes. In this case, codes.NotFound is
the appropriate code.
err := status.Error(codes.NotFound, "id was not found")
return nil, err
To find which code you should be returning when, make sure to read
the extensive documentation for the grpc/codes package.
These errors translate the code and message to the grpc-message
and grpc-status trailers respectively in the
gRPC HTTP2 protocol spec.
Extracting the message and code in a gRPC client is also done through the
status package, with the Status.FromError.
st, ok := status.FromError(err)
if !ok {
// Error was not a status error
}
// Use st.Message() and st.Code()
The Go gRPC implementation guarantees
that all errors returned from RPC calls are status type errors. Because of this,
you can usually use the
status.Convert method instead.
Advanced usage
The status package also comes with the power to attach arbitrary
protobuf metadata to your errors, courtesy of the protobuf Any message type
and the Status.WithDetails
method.
For example, if a request is provided with a parameter that is incorrect regardless
of the state of the system, you may want to return more information about which
field caused the error and why. You could stuff all of this into the error message,
but it is not meant for long messages. Here’s an example of using the
errdetails package
to attach extra error metadata to an error:
st := status.New(codes.InvalidArgument, "invalid username")
desc := "The username must only contain alphanumeric characters"
v := &errdetails.BadRequest_FieldViolation{
Field: "username",
Description: desc,
}
br := &errdetails.BadRequest{}
br.FieldViolations = append(br.FieldViolations, v)
st, err := st.WithDetails(br)
if err != nil {
// If this errored, it will always error
// here, so better panic so we can figure
// out why than have this silently passing.
panic(fmt.Sprintf("Unexpected error attaching metadata: %v", err))
}
return st.Err()
In order to extract these errors on the other side, for printing a nicely
formatted error message to the user for example, you can use the
status.Details method:
st := status.Convert(err)
for _, detail := range st.Details() {
switch t := detail.(type) {
case *errdetails.BadRequest:
fmt.Println("Oops! Your request was rejected by the server.")
for _, violation := range t.GetFieldViolations() {
fmt.Printf("The %q field was wrong:\n", violation.GetField())
fmt.Printf("\t%s\n", violation.GetDescription())
}
}
}
Note about using GoGo Protobuf with status
UPDATE
TL:DR; gogo/googleapis types work with grpc/status.
While investigating another issue
relating to gogo/protobuf and the grpc-gateway, github user
@glerchundi
pointed out
that gogo/protobuf types could potentially circumvent issues with
golang/protobuf/ptypes referring to its own registry by implementing
XXX_MessageName() string on its types. This turned out to fix all compatibility
issues with grpc/status, so gogo/protobuf was quickly updated
to support this function in gogo/protobuf/types and gogo/googleapis.
As a result of this, gogo/googleapis types now work transparently with grpc/status.
gogo/status is still necessary if you want to
use types that only register with gogo/protobuf and don’t make use of either
the goproto_registration or messagename GoGo Protobuf extensions.
I’ve preserved the old advice here, but it no longer applies. The
gogo/grpc-example repo has been updated
to make use of grpc/status again.
I mentioned above that the
statuspackage uses theAnyprotobuf message type under the hood. This, combined with theStatus.WithDetailsandStatus.Detailsmethods using thegolang/protobuf/ptypesdirectly causes it to be generally incompatible withgogo/protobufmessages. One workaround for this is to register your error metadata messages withgolang/protobufthrough thegoproto_registrationextension. This will work for your own types, but what if you don’t have control over the extensions used? What if you want to use types fromgogo/googleapisas I suggested in my post ongogo/protobufcompatibility?
To help with this issue, I submitted a PR to the Go gRPC project to allow the creation of
status.Statustypes from arbitrary error types that implement a specific interface. This, in combination with the newgogo/statuspackage allows the user the same simplestatusinterface that works with arbitrarygogo/protobufregistered message types.
For an example of this in use, please check out the
gogo/grpc-examplerepo, which was created to showcase this and other solutions when usinggogo/protobuf, especially together with the gRPC-Gateway. Please ensure you use gRPC Go v1.11.0 or greater to make use of thegogo/statuspackage.
Further reading
The Google API Design Guide has a section on errors with a thorough discussion of the Status Protobuf type which I encourage you to read if you want to learn more about general protobuf API error handling.
If you enjoyed this blog post, have any questions or input, don’t hesitate to
contact me on @jbrandhorst.com or
under jbrandhorst on the Gophers Slack. I’d love to hear your thoughts!