gRPC Boundaries
go-error-family does not import google.golang.org/grpc — the classification
core stays zero-dependency. Instead, map the family to a gRPC status code at
your server boundary. The mapping below follows Google’s official HTTP ↔ gRPC
code mapping, so a service that
serves both HTTP and gRPC stays consistent.
Family → gRPC Status Mapping
Section titled “Family → gRPC Status Mapping”| Family | Canonical HTTP | gRPC code | Rationale |
|---|---|---|---|
Rejection |
400 | codes.InvalidArgument |
Client sent incomplete or invalid input; retrying unchanged will fail again. |
Conflict |
409 | codes.Aborted |
The asserted state no longer holds (Google maps 409 → ABORTED). If you set WithHTTPStatus(412), use codes.FailedPrecondition instead. |
Transient |
503 | codes.Unavailable |
The only family gRPC clients may retry automatically (service-config retry policies retry UNAVAILABLE). |
Infrastructure |
503 | codes.Unavailable |
Also UNAVAILABLE, but the family’s retry policy is single-attempt — gate client retries on the error’s code, not the gRPC status alone. |
Corruption |
500 | codes.Internal |
Stored data is damaged; never retryable, needs a human. |
Orchestration |
500 | codes.Internal |
The program’s own coordination logic failed; never retryable, report the bug. |
| (unknown) | — | codes.Unknown |
Classify fail-opens to Transient; treat unclassified transport errors as Unknown unless you want the retry behavior. |
Per-error HTTP overrides (WithHTTPStatus) do not change the gRPC mapping —
the interceptor maps from the family. If a specific error needs a different gRPC
code (for example a not-found Rejection as codes.NotFound), switch on the
error code before falling back to the family default.
Unary Interceptor
Section titled “Unary Interceptor”package grpcboundary
import ( "context"
errorfamily "github.com/larsartmann/go-error-family" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status")
func UnaryErrorInterceptor() grpc.UnaryServerInterceptor { return func( ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (any, error) { resp, err := handler(ctx, req) if err == nil { return resp, nil } return resp, toStatus(err) }}
func toStatus(err error) error { family := errorfamily.Classify(err) st := status.New(familyToCode(family), errorfamily.Code(err)) return st.Err()}
func familyToCode(family errorfamily.Family) codes.Code { switch family { case errorfamily.Rejection: return codes.InvalidArgument case errorfamily.Conflict: return codes.Aborted case errorfamily.Transient, errorfamily.Infrastructure: return codes.Unavailable case errorfamily.Corruption, errorfamily.Orchestration: return codes.Internal default: return codes.Unknown }}Two deliberate choices, mirroring the HTTP boundary:
- The status message is the error code, never
err.Error()— internal details (paths, hosts, SQL fragments) must not leak across the wire. - Classification happens once, at the boundary. Domain code keeps returning classified errors; the interceptor is the only place that knows about gRPC.
Retry Guidance
Section titled “Retry Guidance”codes.Unavailableis the code gRPC service-config retry policies act on. ATransientclassification means “retry is safe” (default policy: 3 attempts);Infrastructuremaps to the same code but its retry policy is a single attempt — inspect the error code in the status message when your client needs to tell them apart.codes.Internal,codes.InvalidArgument, andcodes.Abortedmust not be retried without changing the request or resolving the conflict first.errors.Isstill works client-side if both ends use the library: match on code + family to recover typed decisions from astatuserror.
See Also
Section titled “See Also”- HTTP & CLI Boundaries — the HTTP/CLI mapping,
HTTPHandlermiddleware, and template resolution - Classification — how
Classifyreaches a family decision