or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

admin.mdadvanced.mdclient-server.mdcredentials-security.mderrors-status.mdhealth.mdindex.mdinterceptors.mdload-balancing.mdmetadata-context.mdname-resolution.mdobservability.mdreflection.mdstreaming.mdtesting.mdxds.md
tile.json

errors-status.mddocs/

Error Handling and Status Codes

This document covers error handling and status codes in gRPC-Go, including the status package, error codes, error creation and conversion, and best practices.

Status Package

Overview

The status package implements errors returned by gRPC. These errors are serialized and transmitted between server and client, and allow for additional data to be transmitted via the Details field.

import "google.golang.org/grpc/status"

Status Type

type Status = status.Status

Status represents an RPC status code, message, and details. It is immutable and should be created with New, Newf, or FromProto.

The Status type provides these methods:

// Code returns the status code
func (s *Status) Code() codes.Code

// Message returns the status message
func (s *Status) Message() string

// Details returns the status details
func (s *Status) Details() []any

// Proto returns the Status as an spb.Status proto
func (s *Status) Proto() *spb.Status

// Err returns an error representing this status
// Returns nil if Code() is OK
func (s *Status) Err() error

// WithDetails returns a new Status with additional details attached
func (s *Status) WithDetails(details ...proto.Message) (*Status, error)

Status Codes

Code Type

import "google.golang.org/grpc/codes"

type Code uint32

A Code is a status code defined according to the gRPC specification. Only the codes defined as consts are valid codes.

Standard Status Codes

const (
    // OK is returned on success.
    OK Code = 0

    // Canceled indicates the operation was canceled (typically by the caller).
    // The gRPC framework will generate this error code when cancellation is requested.
    Canceled Code = 1

    // Unknown error. Generated when an error-space is not known or APIs don't
    // return enough error information.
    Unknown Code = 2

    // InvalidArgument indicates client specified an invalid argument.
    // This differs from FailedPrecondition - it indicates arguments that are
    // problematic regardless of the state of the system.
    InvalidArgument Code = 3

    // DeadlineExceeded means operation expired before completion.
    // For operations that change state, this error may be returned even if
    // the operation completed successfully.
    DeadlineExceeded Code = 4

    // NotFound means some requested entity was not found.
    NotFound Code = 5

    // AlreadyExists means an attempt to create an entity failed because one
    // already exists.
    AlreadyExists Code = 6

    // PermissionDenied indicates the caller does not have permission to
    // execute the specified operation.
    // Must not be used for rejections caused by exhausting resources.
    // Must not be used if the caller cannot be identified (use Unauthenticated).
    PermissionDenied Code = 7

    // ResourceExhausted indicates some resource has been exhausted, perhaps
    // a per-user quota, or the entire file system is out of space.
    // Generated by gRPC framework in out-of-memory and server overload situations.
    ResourceExhausted Code = 8

    // FailedPrecondition indicates operation was rejected because the
    // system is not in a state required for the operation's execution.
    FailedPrecondition Code = 9

    // Aborted indicates the operation was aborted, typically due to a
    // concurrency issue like sequencer check failures, transaction aborts, etc.
    Aborted Code = 10

    // OutOfRange means operation was attempted past the valid range.
    // E.g., seeking or reading past end of file.
    OutOfRange Code = 11

    // Unimplemented indicates operation is not implemented or not
    // supported/enabled in this service.
    // Generated by gRPC framework when a method implementation is missing.
    Unimplemented Code = 12

    // Internal errors. Means some invariants expected by underlying
    // system has been broken.
    Internal Code = 13

    // Unavailable indicates the service is currently unavailable.
    // This is most likely a transient condition and may be corrected by retrying
    // with a backoff.
    Unavailable Code = 14

    // DataLoss indicates unrecoverable data loss or corruption.
    DataLoss Code = 15

    // Unauthenticated indicates the request does not have valid
    // authentication credentials for the operation.
    Unauthenticated Code = 16
)

// String returns the string representation of the code
func (c Code) String() string

// UnmarshalJSON unmarshals b into the Code
func (c *Code) UnmarshalJSON(b []byte) error

Code Selection Guidelines

Choosing between FailedPrecondition, Aborted, and Unavailable:

  • Use Unavailable if the client can retry just the failing call
  • Use Aborted if the client should retry at a higher-level (e.g., restarting a read-modify-write sequence)
  • Use FailedPrecondition if the client should not retry until the system state has been explicitly fixed

Creating Status Errors

Basic Error Creation

// New returns a Status representing c and msg
func New(c codes.Code, msg string) *Status

// Newf returns New(c, fmt.Sprintf(format, a...))
func Newf(c codes.Code, format string, a ...any) *Status

// Error returns an error representing c and msg
// If c is OK, returns nil
func Error(c codes.Code, msg string) error

// Errorf returns Error(c, fmt.Sprintf(format, a...))
func Errorf(c codes.Code, format string, a ...any) error

Example:

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// Create status with New
st := status.New(codes.NotFound, "user not found")
err := st.Err()

// Create error directly
err := status.Error(codes.InvalidArgument, "email is required")

// Create with formatting
err := status.Errorf(codes.NotFound, "user %s not found", userID)

// Create status from proto
pbStatus := &spb.Status{
    Code:    int32(codes.Internal),
    Message: "internal server error",
}
st := status.FromProto(pbStatus)

Creating Status from Proto

// FromProto returns a Status representing s
func FromProto(s *spb.Status) *Status

// ErrorProto returns an error representing s
// If s.Code is OK, returns nil
func ErrorProto(s *spb.Status) error

Converting Errors to Status

Status Extraction

// Code returns the Code of the error if it is a Status error or if it wraps
// a Status error. If not, returns codes.OK if err is nil, or codes.Unknown
func Code(err error) codes.Code

// FromError returns a Status representation of err
// Returns (status, true) if err is compatible with status package
// Returns (status with codes.Unknown, false) otherwise
func FromError(err error) (s *Status, ok bool)

// Convert is a convenience function which removes the need to handle the
// boolean return value from FromError
func Convert(err error) *Status

// FromContextError converts a context error into a Status
// Returns codes.OK if err is nil
// Returns codes.Canceled if err is context.Canceled
// Returns codes.DeadlineExceeded if err is context.DeadlineExceeded
// Returns codes.Unknown otherwise
func FromContextError(err error) *Status

Example:

import (
    "context"
    "errors"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// Extract status from error
err := someGRPCCall()
st, ok := status.FromError(err)
if ok {
    fmt.Printf("Code: %s, Message: %s\n", st.Code(), st.Message())
}

// Convert any error to status
st := status.Convert(err)
fmt.Printf("Code: %s\n", st.Code())

// Get just the code
code := status.Code(err)
if code == codes.NotFound {
    // Handle not found
}

// Handle context errors
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

err := waitForSomething(ctx)
st := status.FromContextError(ctx.Err())
if st.Code() == codes.DeadlineExceeded {
    fmt.Println("Operation timed out")
}

Status Details

Adding Details to Status

Status messages can include additional structured details using protobuf messages:

// WithDetails returns a new Status with additional details attached
// The details must be valid proto.Message instances
func (s *Status) WithDetails(details ...proto.Message) (*Status, error)

// Details returns the status details as a slice of any
func (s *Status) Details() []any

Example:

import (
    "google.golang.org/genproto/googleapis/rpc/errdetails"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// Create status with details
st := status.New(codes.InvalidArgument, "validation failed")

// Add field violation details
badRequest := &errdetails.BadRequest{
    FieldViolations: []*errdetails.BadRequest_FieldViolation{
        {
            Field:       "email",
            Description: "must be a valid email address",
        },
        {
            Field:       "age",
            Description: "must be positive",
        },
    },
}

st, err := st.WithDetails(badRequest)
if err != nil {
    // Handle error
}

return st.Err()

Extracting Details from Status

// On the client side, extract details
err := client.SomeRPC(ctx, req)
st := status.Convert(err)

for _, detail := range st.Details() {
    switch t := detail.(type) {
    case *errdetails.BadRequest:
        fmt.Println("BadRequest details:")
        for _, violation := range t.GetFieldViolations() {
            fmt.Printf("  %s: %s\n", violation.Field, violation.Description)
        }
    case *errdetails.RetryInfo:
        fmt.Printf("Retry after: %v\n", t.GetRetryDelay())
    }
}

Server-Side Error Handling

Returning Errors from Handlers

Server handlers should return status errors:

import (
    "context"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    if req.Id == "" {
        return nil, status.Error(codes.InvalidArgument, "user id is required")
    }

    user, err := s.db.FindUser(req.Id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, status.Errorf(codes.NotFound, "user %s not found", req.Id)
        }
        return nil, status.Error(codes.Internal, "database error")
    }

    return user, nil
}

Streaming Error Handling

For streaming RPCs, return status errors to terminate the stream:

func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
    users, err := s.db.GetUsers(req.PageSize, req.PageToken)
    if err != nil {
        return status.Error(codes.Internal, "failed to list users")
    }

    for _, user := range users {
        if err := stream.Send(user); err != nil {
            return status.Error(codes.Aborted, "stream interrupted")
        }
    }

    return nil // OK status
}

Client-Side Error Handling

Checking Error Codes

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

resp, err := client.GetUser(ctx, req)
if err != nil {
    // Check specific codes
    if status.Code(err) == codes.NotFound {
        fmt.Println("User not found")
        return nil
    }

    // Get full status for details
    st := status.Convert(err)
    fmt.Printf("RPC failed: %s - %s\n", st.Code(), st.Message())

    // Check for specific errors
    switch st.Code() {
    case codes.Canceled:
        fmt.Println("Request was canceled")
    case codes.DeadlineExceeded:
        fmt.Println("Request timed out")
    case codes.Unavailable:
        fmt.Println("Service unavailable, retry later")
    default:
        fmt.Printf("Unexpected error: %v\n", err)
    }

    return err
}

Implementing GRPCStatus Interface

Custom errors can implement the GRPCStatus interface to be compatible with the status package:

type MyError struct {
    code codes.Code
    msg  string
}

func (e *MyError) Error() string {
    return e.msg
}

// GRPCStatus makes MyError compatible with status package
func (e *MyError) GRPCStatus() *status.Status {
    return status.New(e.code, e.msg)
}

// Usage
err := &MyError{code: codes.InvalidArgument, msg: "invalid input"}
st, ok := status.FromError(err)  // ok will be true

Legacy API (Deprecated)

Deprecated Functions

These functions are deprecated but still supported in grpc-go:

// Deprecated: use status.Code instead
func Code(err error) codes.Code

// Deprecated: use status.Convert and Message method instead
func ErrorDesc(err error) string

// Deprecated: use status.Errorf instead
func Errorf(c codes.Code, format string, a ...any) error

Common Error Patterns

Validation Errors

func validateRequest(req *pb.CreateUserRequest) error {
    if req.Email == "" {
        return status.Error(codes.InvalidArgument, "email is required")
    }
    if !isValidEmail(req.Email) {
        return status.Error(codes.InvalidArgument, "email is invalid")
    }
    if req.Age < 0 {
        return status.Error(codes.InvalidArgument, "age must be positive")
    }
    return nil
}

Database Errors

func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    user, err := s.db.FindUser(ctx, req.Id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, status.Errorf(codes.NotFound, "user %s not found", req.Id)
        }
        // Log internal errors but don't expose details to client
        log.Printf("database error: %v", err)
        return nil, status.Error(codes.Internal, "internal server error")
    }
    return user, nil
}

Permission Errors

func (s *server) DeleteUser(ctx context.Context, req *pb.DeleteUserRequest) (*pb.Empty, error) {
    // Extract user from context (set by auth interceptor)
    userID := getUserIDFromContext(ctx)
    if userID == "" {
        return nil, status.Error(codes.Unauthenticated, "authentication required")
    }

    // Check permissions
    if !s.canDelete(userID, req.Id) {
        return nil, status.Error(codes.PermissionDenied, "insufficient permissions")
    }

    // Perform deletion
    if err := s.db.DeleteUser(req.Id); err != nil {
        return nil, status.Error(codes.Internal, "failed to delete user")
    }

    return &pb.Empty{}, nil
}

Resource Exhaustion

func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
    // Check rate limits
    if !s.rateLimiter.Allow(getUserIDFromContext(ctx)) {
        return nil, status.Error(codes.ResourceExhausted, "rate limit exceeded")
    }

    // Check quotas
    if s.db.UserCount() >= s.maxUsers {
        return nil, status.Error(codes.ResourceExhausted, "user quota exceeded")
    }

    user, err := s.db.CreateUser(req)
    if err != nil {
        return nil, status.Error(codes.Internal, "failed to create user")
    }

    return user, nil
}

Best Practices

Error Code Selection

  1. Use appropriate codes: Select the most specific error code that matches the error condition
  2. Be consistent: Use the same error codes for the same types of errors across your service
  3. Client perspective: Choose codes based on what the client needs to know, not implementation details

Error Messages

  1. Be descriptive: Provide enough information for debugging without exposing sensitive data
  2. Don't leak internals: Avoid exposing database queries, file paths, or internal implementation details
  3. Include context: Include relevant identifiers (e.g., user IDs) in error messages
  4. Be actionable: Help clients understand what they can do to fix the error

Security Considerations

  1. Mask internal errors: Return generic codes.Internal for unexpected errors, log details server-side
  2. Avoid information leakage: Don't reveal whether resources exist through different error codes
  3. Rate limiting: Use codes.ResourceExhausted for rate limit errors
  4. Authentication vs Authorization: Use codes.Unauthenticated when credentials are missing/invalid, codes.PermissionDenied when authenticated but unauthorized

Testing

import (
    "testing"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func TestGetUser_NotFound(t *testing.T) {
    s := &server{db: &mockDB{}}
    _, err := s.GetUser(context.Background(), &pb.GetUserRequest{Id: "nonexistent"})

    if err == nil {
        t.Fatal("expected error, got nil")
    }

    st := status.Convert(err)
    if st.Code() != codes.NotFound {
        t.Errorf("expected NotFound, got %s", st.Code())
    }

    if !strings.Contains(st.Message(), "nonexistent") {
        t.Errorf("error message should include user ID: %s", st.Message())
    }
}