protoc-gen-sphere-errors
protoc-gen-sphere-errors is a protoc plugin that generates error handling code from .proto files. It is designed to inspect enum definitions within your protobuf files and automatically generate corresponding error handling code based on the sphere errors framework. This plugin creates Go code that provides structured error handling with HTTP status codes, error codes, and customizable messages.
This code is inspired by protoc-gen-go-errors but is specifically designed for the go-sphere framework.
Features
- Generates helpers with HTTP status codes, application codes, and messages
- Uses
reasoninError()when set; otherwise uses the enum value name - Provides
JoinandJoinWithMessagemethods for error composition viahttpx.NewError - Supports default status codes for enum types
- Deduplicates
allow_aliasenum values so generated switches compile - Skips the zero-value
*_UNSPECIFIEDentry as a returned business error in generated helpers
Configuration Parameters
version: Print the plugin version and exit. (Default:false)new_errors_func: Constructor used byJoin/JoinWithMessage. Format:import/path;Ident. (Default:github.com/go-sphere/httpx;NewError)template_file: Path to a custom Go template. Empty uses the embedded default.
Installation
To install protoc-gen-sphere-errors, use the following command:
go install github.com/go-sphere/protoc-gen-sphere-errors@latestPrerequisites
You need to have the sphere errors proto definitions in your project. Add the following dependency to your buf.yaml:
deps:
- buf.build/go-sphere/errorsUsage with Buf
To use protoc-gen-sphere-errors with buf, you can configure it in your buf.gen.yaml file. Here is an example configuration:
version: v2
managed:
enabled: true
disable:
- file_option: go_package_prefix
module: buf.build/go-sphere/errors
override:
- file_option: go_package_prefix
value: github.com/go-sphere/sphere-layout/api
plugins:
- local: protoc-gen-sphere-errors
out: api
opt:
- paths=source_relativeProto Definition Example
Here’s how to define error enums in your .proto files:
syntax = "proto3";
package shared.v1;
import "sphere/errors/errors.proto";
enum TestError {
option (sphere.errors.default_status) = 500;
TEST_ERROR_UNSPECIFIED = 0;
TEST_ERROR_INVALID_PATH_TEST2 = 1001 [(sphere.errors.options) = {
status: 400
reason: "INVALID_PATH"
message: "Invalid path parameter"
}];
TEST_ERROR_MISSING_FIELD = 1002 [(sphere.errors.options) = {
status: 400
reason: "MISSING_FIELD"
message: "Required field is missing"
}];
}
enum UserError {
option (sphere.errors.default_status) = 500;
USER_ERROR_UNSPECIFIED = 0;
USER_ERROR_NOT_FOUND = 2001 [(sphere.errors.options) = {
status: 404
reason: "USER_NOT_FOUND"
message: "User not found"
}];
USER_ERROR_INVALID_EMAIL = 2002 [(sphere.errors.options) = {
status: 400
reason: "INVALID_EMAIL"
message: "Invalid email format"
}];
USER_ERROR_ALREADY_EXISTS = 2003 [(sphere.errors.options) = {
status: 409
reason: "USER_EXISTS"
message: "User already exists"
}];
}Generated Code
The plugin generates Go code with the following methods for each error enum:
Error() string— implementserror. Returnsreasonwhen set, otherwise the enum value name.GetCode() int32— the numeric enum value (e.g.1001)GetStatus() int32— the HTTP status codeGetMessage() string— the user-facing default messageJoin(errs ...error) error— wraps causes withhttpx.NewErrorJoinWithMessage(msg string, errs ...error) error— same, with a runtime message
There is no generated GetReason() method. reason is only used as the Error() string. The enum itself implements httpx.StatusError, httpx.CodeError, and httpx.MessageError, so returning it directly from a service method is valid.
Usage in Code
Direct Error Returns
func (s *service) ValidateField(field string) error {
if field == "" {
return sharedv1.TestError_TEST_ERROR_MISSING_FIELD
}
return nil
}Error Handling in HTTP Handlers
func (s *service) RunTest(ctx context.Context, req *sharedv1.RunTestRequest) (*sharedv1.RunTestResponse, error) {
if req.FieldTest1 == "" {
return nil, sharedv1.TestError_TEST_ERROR_MISSING_FIELD.Join(
fmt.Errorf("field_test1 cannot be empty"))
}
// Business logic...
user, err := s.userRepo.GetByID(ctx, req.UserId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, sharedv1.UserError_USER_ERROR_NOT_FOUND.Join(err)
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
return &sharedv1.RunTestResponse{
FieldTest1: req.FieldTest1,
}, nil
}Custom Messages
func (s *service) CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) {
if !isValidEmail(req.Email) {
return nil, sharedv1.UserError_USER_ERROR_INVALID_EMAIL.JoinWithMessage(
fmt.Sprintf("Email '%s' does not match required format", req.Email), nil)
}
// Check if user exists
existing, _ := s.userRepo.GetByEmail(ctx, req.Email)
if existing != nil {
return nil, sharedv1.UserError_USER_ERROR_ALREADY_EXISTS.JoinWithMessage(
fmt.Sprintf("User with email '%s' already exists", req.Email), nil)
}
// Create user...
}HTTP Response Integration
When used with Sphere’s HTTP server utilities, these errors are automatically converted to structured JSON responses:
{
"success": false,
"code": 2001,
"message": "User not found"
}ErrorResponse.Error (err.Error()) is added only when httpz.SetDebugMode(true). Unclassified errors (plain fmt.Errorf, driver errors) report code: 0 and a generic HTTP status message. See Error Handling and HTTP Runtime.
Best Practices
- Use meaningful error codes: Choose enum values that clearly indicate the error type
- Set appropriate HTTP status codes: Use standard HTTP status codes (400, 401, 403, 404, 500, etc.)
- Provide clear messages: Write user-friendly error messages that can be displayed to end users
- Use reasons for programmatic handling: Include reason strings that client applications can use for conditional logic
- Group related errors: Keep related errors in the same enum for better organization
- Preserve context with Join: Always use
.Join()to wrap underlying errors for better debugging
Integration Notes
httpz.WithJsonmaps these errors to structured JSON with the correct HTTP status- Pair with
httpz.SetDefaultErrorParserif you need to merge validation, not-found, or domain errors - The generated errors implement
httpx.Errorinterfaces so the runtime can read status, code, and message without extra wrapping