This document covers error handling and status codes in gRPC-Go, including the status package, error codes, error creation and conversion, and best practices.
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"type Status = status.StatusStatus 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)import "google.golang.org/grpc/codes"
type Code uint32A Code is a status code defined according to the gRPC specification. Only the codes defined as consts are valid 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) errorChoosing between FailedPrecondition, Aborted, and Unavailable:
Unavailable if the client can retry just the failing callAborted if the client should retry at a higher-level (e.g., restarting a read-modify-write sequence)FailedPrecondition if the client should not retry until the system state has been explicitly fixed// 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) errorExample:
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)// 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// 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) *StatusExample:
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 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() []anyExample:
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()// 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 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
}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
}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
}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 trueThese 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) errorfunc 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
}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
}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
}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
}codes.Internal for unexpected errors, log details server-sidecodes.ResourceExhausted for rate limit errorscodes.Unauthenticated when credentials are missing/invalid, codes.PermissionDenied when authenticated but unauthorizedimport (
"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())
}
}