The Go gRPC status package gives service errors a code and descriptive message instead of leaving callers to interpret an unstructured error string. A caller can use the code to decide what to do next; for example, a ResourceExhausted response may call for a later retry rather than immediate failure. section 5.4 section 6.2
At a service boundary, resolve a downstream error with status.FromError when its code and message are enough. To preserve more context, create a new status with status.New, attach a detail with WithDetails, and return the resulting error. An Order service can mark a Payment failure as a payment field violation while reporting that order creation failed. section 5.3.3 section 5.3.4
func (a Application) PlaceOrder(order domain.Order) (domain.Order, error) {
err := a.db.Save(&order)
if err != nil {
return domain.Order{}, err
}
paymentErr := a.payment.Charge(&order)
if paymentErr != nil {
st, _ := status.FromError(paymentErr) #1
fieldErr := &errdetails.BadRequest_FieldViolation{
Field: "payment",
Description: st.Message(),
} #2
badReq := &errdetails.BadRequest{} #3
badReq.FieldViolations = append(badReq.FieldViolations, fieldErr) #4
orderStatus := status.New(codes.InvalidArgument, "order creation
failed") #5
statusWithDetails, _ := orderStatus.WithDetails(badReq) #6
return domain.Order{}, statusWithDetails.Err()
}
return order, nil
}
The PlaceOrder listing shows a Payment error being resolved, placed in a BadRequest field violation, and attached to an InvalidArgument order status. section 5.3.3
When downstream errors already carry details, status.Convert provides a status whose Details can be inspected. The Order service example extracts BadRequest field violations before grouping their descriptions under the payment field. This preserves useful failure information across service boundaries instead of relying only on a single message. section 5.3.4
func (a Application) PlaceOrder(order domain.Order) (domain.Order, error) {
err := a.db.Save(&order)
if err != nil {
return domain.Order{}, err
}
paymentErr := a.payment.Charge(&order)
if paymentErr != nil {
st := status.Convert(paymentErr) #1
var allErrors []string #2
for _, detail := range st.Details() {
switch t := detail.(type) {
case *errdetails.BadRequest: #3
for _, violation := range t.GetFieldViolations() {
allErrors = append(allErrors, violation.Description)
}
}
}
fieldErr := &errdetails.BadRequest_FieldViolation{
Field: "payment",
Description: strings.Join(allErrors, "\n"),
} #4
badReq := &errdetails.BadRequest{}
badReq.FieldViolations = append(badReq.FieldViolations, fieldErr)
orderStatus := status.New(codes.InvalidArgument, "order creation
failed") #5
statusWithDetails, _ := orderStatus.WithDetails(badReq) #6
return domain.Order{}, statusWithDetails.Err()
}
return order, nil
}
The client-side listing shows status.Convert and a type switch over BadRequest details to collect field-violation descriptions. section 5.3.4
