Lesson 12 · Errors & idiomatic Go
gRPC status codes
Turning a Go error into a response the client can branch on — at the right layer.
Your win: map a domain error to a codes.* status the way
our services do, and explain why that mapping lives only at the controller
boundary — not deep in the usecase.
gRPC speaks in status codes
A gRPC handler doesn't return a bare Go error to the client — it returns a
status: a codes.Code plus a message, built with
status.Error(code, msg).1 The code
is what lets the caller branch (retry? show a 400? deny?), so choosing the right one
matters.
Map at the boundary, nowhere else
codes.*. That keeps business logic reusable
from a job, a NATS handler, or another transport.
Our SendEmail handler is the template: it uses errors.Is
(Lesson 11) on the sentinels the validator returned, and picks a code per case:
if err := svc.validateSendEmailPayloads(ctx, ...); err != nil {
if errors.Is(err, ErrMediasSizeExceed) || errors.Is(err, ErrEmailsCountExceed) {
return nil, status.Error(codes.ResourceExhausted, err.Error())
}
if errors.Is(err, ErrInternal) {
return nil, status.Error(codes.Internal, err.Error())
}
return nil, status.Error(codes.InvalidArgument, err.Error())
}
The repo's code map (from .claude/rules/security.md)
| Situation | Code |
|---|---|
| Bad/invalid input | codes.InvalidArgument |
| Infra / unexpected failure | codes.Internal |
| Size / rate limit exceeded | codes.ResourceExhausted |
| Entity missing | codes.NotFound |
| Any auth failure | codes.PermissionDenied — never Internal |
errorx.ToStatusError
(internal/golibs/errorx/errorx.go:56-78) uses
errors.As to read a *pgconn.PgError and map it:
UniqueViolation → codes.AlreadyExists (with
PreconditionFailure details), ForeignKeyViolation →
codes.InvalidArgument. One place, reused everywhere.
Internal (it must be PermissionDenied). This is both an
idiom and a security rule in our repo.
gRPC codes package + status semantics
The canonical list of codes and exactly what each means — so your mapping matches what clients expect across languages.
→ pkg.go.dev/google.golang.org/grpc/codes
→ grpc/status (building & reading status)
Check yourself (from memory)
Q1. In our services, errors are mapped to gRPC status codes…
Q2. A validation failure should map to…
InvalidArgument. Size/rate
limits → ResourceExhausted; infra → Internal.
Q3. An authentication/authorization failure must map to…
PermissionDenied — never disguised as Internal.
codes.*
there (via errors.Is/As). Centralises the mapping and keeps business logic
clean. Bonus: never leak internals in the message, and auth failures →
PermissionDenied.