Handling Errors and Status Codes

1. Creating Status Errors

APIUse
status.Error(code, msg)Simple error
status.Errorf(code, fmt, ...)Formatted
status.New(code, msg).WithDetails(...)With error details

2. Using Standard Status Codes

CodeMeaningHTTP-like Analog
OK (0)Success200
Canceled (1)Client cancelled499
Unknown (2)Unmapped error500
InvalidArgument (3)Bad input400
DeadlineExceeded (4)Timeout504
NotFound (5)Missing resource404
AlreadyExists (6)Duplicate409
PermissionDenied (7)AuthZ failed403
ResourceExhausted (8)Quota/rate limit429
FailedPrecondition (9)State invalid400
Aborted (10)Concurrency conflict409
OutOfRange (11)Range error400
Unimplemented (12)Method not impl501
Internal (13)Server bug500
Unavailable (14)Service down — retryable503
DataLoss (15)Unrecoverable corruption500
Unauthenticated (16)Missing creds401

3. Adding Error Details

Detail TypePurpose
BadRequestField violations
ErrorInfoDomain + reason code
RetryInfoSuggested retry delay
QuotaFailureQuota exceeded specifics
PreconditionFailureState issues
ResourceInfoResource type + name
HelpDoc links
LocalizedMessagei18n message

4. Extracting Status from Error

Example: Status extraction

st, ok := status.FromError(err)
if ok {
    fmt.Println(st.Code(), st.Message())
    for _, d := range st.Details() {
        switch info := d.(type) {
        case *errdetails.BadRequest:
            // handle field violations
        case *errdetails.RetryInfo:
            time.Sleep(info.RetryDelay.AsDuration())
        }
    }
}

5. Creating Custom Error Details

Example: Custom detail message

// in proto:
message AppError {
  string code = 1;
  string trace_id = 2;
}
st, _ := status.New(codes.Internal, "db down").
    WithDetails(&AppError{Code: "DB_001", TraceId: traceID})
return st.Err()

6. Checking Specific Status Codes

Example: Code-based handling

switch status.Code(err) {
case codes.NotFound:
    return defaultUser(), nil
case codes.Unavailable, codes.DeadlineExceeded:
    return retry()
default:
    return nil, err
}

7. Handling Canceled Errors

SourceAction
Client canceledTreat as normal — don't log as error
Upstream canceledPropagate as codes.Canceled

8. Handling Timeout Errors

StrategyDetail
Retry with new deadlineIf idempotent
Surface to userFor non-idempotent ops
Circuit breakerDetect repeated timeouts

9. Propagating Errors

RuleDetail
Preserve statusDon't wrap with fmt.Errorf if returning to gRPC client
Map domain → statusAt service boundary only
SanitizeDon't leak stack traces or DB errors

10. Converting Errors

Example: Domain → gRPC mapping

func toStatus(err error) error {
    switch {
    case errors.Is(err, repo.ErrNotFound):
        return status.Error(codes.NotFound, "not found")
    case errors.Is(err, repo.ErrConflict):
        return status.Error(codes.AlreadyExists, "exists")
    case errors.Is(err, context.DeadlineExceeded):
        return status.Error(codes.DeadlineExceeded, "deadline")
    default:
        return status.Error(codes.Internal, "internal")
    }
}