gRPC Error Codes & Status Mapping
Errors cross process boundaries as gRPC status codes, not Go error strings.
Search across all documentation pages
Errors cross process boundaries as gRPC status codes, not Go error strings.
Clients must inspect codes; servers must map domain failures deliberately.
google.golang.org/grpc/status wraps a codes.Code, message, and optional detail protobufs.
Servers return status.Error or status.Errorf; clients unwrap with status.FromError.
Sixteen standard codes cover most distributed systems scenarios.
Structured details (google.rpc.ErrorInfo, custom protos) let UIs and retries act on machine-readable fields.
Treating gRPC errors like opaque fmt.Errorf chains loses information at every hop.
Quick-reference recipe card - copy-paste ready.
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func findUser(id string) error {
if id == "" {
return status.Error(codes.InvalidArgument, "id required")
}
if !exists(id) {
return status.Error(codes.NotFound, "user not found")
}
return nil
}// Client
st, ok := status.FromError(err)
if ok && st.Code() == codes.NotFound {
// handle missing resource
}When to reach for this:
InvalidArgument) vs missing entities (NotFound).ResourceExhausted) or maintenance (Unavailable) for retry logic.ErrorInfo with reason and domain for API gateways.package main
import (
"context"
"fmt"
"log"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/anypb"
)
type inventoryServer struct{}
func (s *inventoryServer) Reserve(ctx context.Context, sku string, qty int32) error {
if qty <= 0 {
st := status.New(codes.InvalidArgument, "quantity must be positive")
br := &errdetails.BadRequest{
FieldViolations: []*errdetails.BadRequest_FieldViolation{
{Field: "qty", Description: "must be > 0"},
},
}
st, err := st.WithDetails(br)
if err != nil {
return status.Error(codes.Internal, "detail attach failed")
}
return st.Err()
}
if !inStock(sku, qty) {
info := &errdetails.ErrorInfo{
Reason: "INSUFFICIENT_STOCK",
Domain: "inventory.example.com",
Metadata: map[string]string{"sku": sku},
}
st, _ := status.New(codes.FailedPrecondition, "not enough stock").WithDetails(info)
return st.Err()
}
return nil
}
func clientHandle(err error) {
st, ok := status.FromError(err)
if !ok {
log.Fatal(err)
}
fmt.Println("code:", st.Code(), "msg:", st.Message())
for _, d := range st.Details() {
switch info := d.(type) {
case *errdetails.ErrorInfo:
fmt.Println("reason:", info.GetReason())
case *errdetails.BadRequest:
for _, v := range info.GetFieldViolations() {
fmt.Println(v.GetField(), v.GetDescription())
}
default:
if any, ok := d.(*anypb.Any); ok {
fmt.Println("unknown detail type:", any.GetTypeUrl())
}
}
}
}
func inStock(sku string, qty int32) bool { return false }
func main() {
srv := &inventoryServer{}
err := srv.Reserve(context.Background(), "ABC", 0)
clientHandle(err)
}What this demonstrates:
status.New plus WithDetails attaches standard detail messages.st.Details() with type switches.FailedPrecondition signals business rule violations distinct from NotFound.BadRequest details familiar to grpc-gateway JSON clients.On failure, gRPC sends a status protobuf in trailers (HTTP/2 headers).
Go surfaces it as error satisfying status.FromError.
Wrapping with %w preserves status if the wrapper chain still exposes a gRPC status.
Plain errors.New becomes codes.Unknown unless converted.
| Code | Meaning | HTTP analog (gateway) | Retry? |
|---|---|---|---|
OK | Success | 200 | - |
InvalidArgument | Bad input | 400 | No |
NotFound | Missing resource | 404 | No |
AlreadyExists | Duplicate create | 409 | No |
PermissionDenied | Authz failure | 403 | No |
Unauthenticated | Authn failure | 401 | No |
ResourceExhausted | Rate limit / quota | 429 | Backoff |
Unavailable | Transient outage | 503 | Yes, with jitter |
DeadlineExceeded | Timeout | 504 | Maybe |
Canceled | Client cancelled | 499 | No |
Internal | Server bug | 500 | No |
// Preserve status through helper layers
func wrap(err error, msg string) error {
if err == nil {
return nil
}
if _, ok := status.FromError(err); ok {
return fmt.Errorf("%s: %w", msg, err)
}
return status.Errorf(codes.Internal, "%s: %v", msg, err)
}codes.Internal for predictable validation errors - clients cannot remediate.status.Convert when bridging legacy APIs.status.Code(err).Internal errors. Fix: recovery interceptor plus explicit status mapping.Unknown - Retry storms and bad alerts. Fix: pick the closest standard code.google.rpc types in gateway config.context.Canceled vs codes.Canceled - Client disconnect may surface differently. Fix: normalize in client libraries.| Alternative | Use When | Don't Use When |
|---|---|---|
| gRPC status + details | All gRPC services | - |
error return only in-process | Private packages never crossing RPC | Public service boundaries |
| HTTP problem+json at edge | Browser-only BFF | Internal protobuf RPC |
| Result enums in response | Business outcomes are expected branches | True failures (auth, bugs) |
Return status.Error values as error.
Callers use the same error interface everywhere.
Log full errors server-side.
Return generic Internal messages to clients unless safe details are intentional.
FailedPrecondition means do not retry until system state changes.
Aborted suggests retry may succeed immediately (concurrency conflict).
Yes - pack with anypb.New and register types clients understand.
Default rules map gRPC codes to HTTP status.
Override in google.api.http annotations when needed.
Use status.FromError first.
Wrapped status errors may need custom Is logic.
One InvalidArgument with multiple BadRequest field violations is idiomatic.
No - alert and surface failure.
Retry Unavailable with capped exponential backoff.
Often as the error return from the final Recv or Send.
Trailers carry status after stream half-close.
Status errors participate in wrapping.
Domain packages can define sentinel errors converted at the transport boundary.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 18, 2026