← Files GophersARCHIVED FILE

skills/go-grpc/references/status-and-errors.md

4.07 KB · Oct 5, 2026 · 18:31 UTC

↓ Download file

# gRPC Status Codes and Rich Errors

The status code is the only piece of error information that travels reliably across language boundaries. The message is a string; clients write retry logic against the code.

## The Code Cheat Sheet

| Code | Meaning | Retry? |
|---|---|---|
| `OK` | Success | — |
| `Canceled` | Caller canceled | No (caller's choice) |
| `Unknown` | Default for raw `error`; means "we don't know what happened" | No |
| `InvalidArgument` | Malformed request (validation) | No |
| `DeadlineExceeded` | Timeout | Caller's policy |
| `NotFound` | Entity does not exist | No |
| `AlreadyExists` | Create conflict | No |
| `PermissionDenied` | Authenticated, lacks permission | No |
| `ResourceExhausted` | Rate-limited or quota exceeded | Backoff + retry |
| `FailedPrecondition` | System not in required state (e.g., empty bucket) | No |
| `Aborted` | Concurrency conflict (e.g., optimistic lock) | Yes |
| `OutOfRange` | Range error (seek past end) | No |
| `Unimplemented` | Method missing on this server | No |
| `Internal` | Server bug or invariant violation | No |
| `Unavailable` | Dependency down, transient | Yes (with backoff) |
| `DataLoss` | Unrecoverable data corruption | No |
| `Unauthenticated` | Missing/invalid credentials | No (refresh first) |

## Mapping Domain Errors

Centralize the mapping. One function from domain error → gRPC error:

```go
func toGRPC(err error) error {
    if err == nil { return nil }

    switch {
    case errors.Is(err, ErrNotFound):
        return status.Errorf(codes.NotFound, "%v", err)
    case errors.Is(err, ErrAlreadyExists):
        return status.Errorf(codes.AlreadyExists, "%v", err)
    case errors.Is(err, ErrForbidden):
        return status.Errorf(codes.PermissionDenied, "%v", err)
    case errors.Is(err, ErrRateLimited):
        return status.Errorf(codes.ResourceExhausted, "%v", err)
    }

    var ve *ValidationError
    if errors.As(err, &ve) {
        st, _ := status.New(codes.InvalidArgument, "validation failed").
            WithDetails(&errdetails.BadRequest{
                FieldViolations: ve.Violations(),
            })
        return st.Err()
    }

    return status.Errorf(codes.Internal, "%v", err)
}
```

Every RPC ends with `return toGRPC(err)` and the body is otherwise plain Go.

## Rich Details with `errdetails`

`google.golang.org/genproto/googleapis/rpc/errdetails` defines well-known detail messages: `BadRequest`, `QuotaFailure`, `RetryInfo`, `LocalizedMessage`, `PreconditionFailure`. They are extracted client-side via `status.FromError(err).Details()`.

```go
st, _ := status.New(codes.InvalidArgument, "validation failed").WithDetails(
    &errdetails.BadRequest{
        FieldViolations: []*errdetails.BadRequest_FieldViolation{
            {Field: "email", Description: "is required"},
            {Field: "age",   Description: "must be ≥ 18"},
        },
    },
    &errdetails.LocalizedMessage{Locale: "en-US", Message: "Please fix the fields below."},
)
return nil, st.Err()
```

Use details for structured information clients can act on. Stack traces belong in logs, not in details.

## Client-Side Inspection

```go
resp, err := client.GetUser(ctx, req)
if err != nil {
    st, _ := status.FromError(err)
    switch st.Code() {
    case codes.NotFound:
        return nil, ErrNotFound
    case codes.Unavailable:
        return nil, errRetryable
    default:
        for _, d := range st.Details() {
            if br, ok := d.(*errdetails.BadRequest); ok {
                return nil, validationFromBR(br)
            }
        }
        return nil, fmt.Errorf("rpc: %w", err)
    }
}
```

## What Not To Do

- **Do not pack stack traces into the status message.** It leaks internals and bloats wire size. Log the trace server-side; return a short cause.
- **Do not return `codes.Internal` from every failure.** Retry policies key off the code; "everything is Internal" disables automatic retries on `Unavailable`.
- **Do not lose the wrapped error.** Keep `fmt.Errorf("...: %w", err)` for server logs, and convert only at the RPC boundary.
- **Do not invent new codes.** Stick to the 16 in `codes`. Custom semantics live in `errdetails`.

SHA-256: e47f9d844a112d0b77758b3d523e496f6774a6251fc1bd26b476153dca44abb9