package errors import ( "errors" "fmt" "go.uber.org/zap" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type ErrorType int const ( BusinessError ErrorType = iota InternalErrorType ) type AppError struct { Code string Message string Type ErrorType Err error } func (e *AppError) Error() string { if e.Err != nil { return fmt.Sprintf("%s: %s (%v)", e.Code, e.Message, e.Err) } return fmt.Sprintf("%s: %s", e.Code, e.Message) } func (e *AppError) Unwrap() error { return e.Err } func NewBusinessError(code, message string) *AppError { return &AppError{ Code: code, Message: message, Type: BusinessError, } } func NewInternalError(code, message string, err error) *AppError { return &AppError{ Code: code, Message: message, Type: InternalErrorType, Err: err, } } func IsBusinessError(err error, code string) bool { var appErr *AppError if !errors.As(err, &appErr) { return false } return appErr.Type == BusinessError && appErr.Code == code } func ToGRPCError(err error, zapLogger *zap.Logger, method string) error { if err == nil { return nil } var appErr *AppError ok := errors.As(err, &appErr) if !ok { if zapLogger != nil { zapLogger.Error("gRPC handler error: unknown error type", zap.String("method", method), zap.Error(err), ) } return status.Error(codes.Internal, "internal server error") } if appErr.Type == InternalErrorType { if zapLogger != nil { zapLogger.Error("gRPC handler error: internal error", zap.String("method", method), zap.String("code", appErr.Code), zap.String("message", appErr.Message), zap.Error(appErr.Err), ) } return status.Error(codes.Internal, "internal server error") } var grpcCode codes.Code switch appErr.Code { case AuthInvalidCredentials, AuthMissing, AuthInvalidToken, RefreshInvalid: grpcCode = codes.Unauthenticated case PermissionDenied: grpcCode = codes.PermissionDenied case InviteLimitReached: grpcCode = codes.ResourceExhausted case InsufficientBalance: grpcCode = codes.FailedPrecondition case InviteInvalidOrExpired: grpcCode = codes.NotFound case EmailAlreadyExists: grpcCode = codes.AlreadyExists case UserNotFound, RequestNotFound: grpcCode = codes.NotFound default: grpcCode = codes.Unknown } st, err := status.New(grpcCode, appErr.Message).WithDetails(&errdetails.ErrorInfo{ Reason: appErr.Code, }) if err != nil { return status.Error(grpcCode, appErr.Message) } return st.Err() }