Error Handling
Sphere provides a powerful mechanism for generating typed, consistent error-handling code directly from your .proto
definitions. By defining your errors as enums, you can ensure that error codes, HTTP statuses, and messages are standardized across your application.
This process is handled by protoc-gen-sphere-errors
, a protoc
plugin that inspects your .proto
files and generates Go error-handling code.
Installation
To install protoc-gen-sphere-errors
, use the following command:
go install github.com/go-sphere/protoc-gen-sphere-errors@latest
Configuration with Buf
To integrate the generator with buf
, add the plugin to your buf.gen.yaml
file. This configuration tells buf
how to execute the plugin and where to place the generated files.
version: v2
managed:
enabled: true
plugins:
- local: protoc-gen-sphere-errors
out: api
opt:
- paths=source_relative
Defining Errors in .proto
Errors are defined as enum
types in your .proto
files. You can use custom options from sphere/errors/errors.proto
to attach metadata like HTTP status codes and default messages to each error.
First, import the necessary definitions in your .proto
file:
import "sphere/errors/errors.proto";
Next, define an enum
for your errors.
Example: Basic Error Enum
Here is an example of an error enum:
syntax = "proto3";
package shared.v1;
import "sphere/errors/errors.proto";
enum UserError {
option (sphere.errors.default_status) = 500; // Default status for all values
USER_ERROR_UNSPECIFIED = 0;
USER_ERROR_NOT_FOUND = 1001 [(sphere.errors.options) = {
status: 404
message: "User not found"
}];
USER_ERROR_INVALID_EMAIL = 1002 [(sphere.errors.options) = {
status: 400
reason: "INVALID_EMAIL"
message: "Invalid email format"
}];
USER_ERROR_ALREADY_EXISTS = 1003 [(sphere.errors.options) = {
status: 409
reason: "USER_EXISTS"
message: "User already exists"
}];
}
Advanced Example with Reasons
enum PaymentError {
option (sphere.errors.default_status) = 500;
PAYMENT_ERROR_UNSPECIFIED = 0;
PAYMENT_ERROR_INSUFFICIENT_FUNDS = 2001 [(sphere.errors.options) = {
status: 402
reason: "INSUFFICIENT_FUNDS"
message: "Insufficient funds in account"
}];
PAYMENT_ERROR_CARD_DECLINED = 2002 [(sphere.errors.options) = {
status: 402
reason: "CARD_DECLINED"
message: "Payment card was declined"
}];
PAYMENT_ERROR_INVALID_AMOUNT = 2003 [(sphere.errors.options) = {
status: 400
reason: "INVALID_AMOUNT"
message: "Payment amount must be positive"
}];
}
Annotation Reference
(sphere.errors.default_status)
: An enum-level option that sets the default HTTP status code for all values. If an error value does not have a specific status, this one will be used.(sphere.errors.options)
: A value-level option to customize a specific error.status
: The HTTP status code (e.g.,400
,404
,500
).reason
: A machine-readable reason code for programmatic error handling.message
: A user-facing default error message.
Using the Generated Code
After running buf generate
, the plugin will create a file named {proto_name}_errors.pb.go
(e.g., user_errors.pb.go
). This file contains a Go enum and several helper methods that allow you to use it as a standard Go error.
Generated Methods
For each enum UserError
, the following methods are generated:
Error() string
: Returns the error reason, making the type compatible with Go’serror
interface. Ifreason
is not set, it returns a string representation of the enum value.GetCode() int32
: Returns the numeric enum value (e.g.,1001
).GetStatus() int32
: Returns the configured HTTP status code.GetMessage() string
: Returns the default error message.GetReason() string
: Returns the error reason (if specified).Join(errs ...error) error
: Wraps one or more source errors, returning astatuserr.Error
that includes the code, status, and message from the enum. This is the recommended way to return an error while preserving the original cause.JoinWithMessage(msg string, errs ...error) error
: Similar toJoin
, but allows you to provide a custom, dynamic message at runtime.
Example: Returning an Error in Go
In your service implementation, you can now return one of the generated errors.
package service
import (
"context"
"fmt"
sharedv1 "myproject/api/shared/v1" // Import the generated package
)
func (s *UserService) GetUser(ctx context.Context, req *GetUserRequest) (*User, error) {
if req.Id <= 0 {
return nil, sharedv1.UserError_USER_ERROR_INVALID_ID.Join(
fmt.Errorf("user ID must be positive, got: %d", req.Id))
}
user, err := s.userRepo.GetByID(ctx, req.Id)
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 user, nil
}
func (s *UserService) 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' is not valid", req.Email), nil)
}
// Check if user already exists
existing, _ := s.userRepo.GetByEmail(ctx, req.Email)
if existing != nil {
return nil, sharedv1.UserError_USER_ERROR_ALREADY_EXISTS.Join(
fmt.Errorf("user with email %s already exists", req.Email))
}
user, err := s.userRepo.Create(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
HTTP Error Response
When this error is handled by Sphere’s server layer, it will automatically be converted into an HTTP response with the appropriate status code and JSON body:
{
"status": 404,
"code": 1001,
"error": "USER_NOT_FOUND",
"message": "User not found"
}
Error Configuration Options
Enum Level Options
default_status
: Sets the default HTTP status code for all enum values that don’t specify their own status
Enum Value Options
status
: HTTP status code (overrides default_status)reason
: Optional machine-readable reason codemessage
: Human-readable error message for client display
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 in the appropriate language
- Use reasons for API consumers: Include reason strings for programmatic error handling
- Group related errors: Keep related errors in the same enum for better organization
- Preserve original errors: Always use
.Join()
to wrap underlying errors for better debugging
Common HTTP Status Codes
400
: Bad Request - Client error, invalid input401
: Unauthorized - Authentication required403
: Forbidden - Permission denied404
: Not Found - Resource doesn’t exist409
: Conflict - Resource conflict422
: Unprocessable Entity - Validation failed429
: Too Many Requests - Rate limiting500
: Internal Server Error - Server-side error502
: Bad Gateway - External service error503
: Service Unavailable - Service temporarily down
Integration with buf
Add the required dependency to your buf.yaml
:
version: v2
deps:
- buf.build/go-sphere/errors
Configure the plugin in your buf.gen.yaml
:
version: v2
managed:
enabled: true
plugins:
- local: protoc-gen-sphere-errors
out: api
opt:
- paths=source_relative
Error Composition
You can compose multiple errors using the generated methods:
// Simple error with context
return nil, UserError_USER_ERROR_NOT_FOUND.Join(err)
// Error with custom message
return nil, UserError_USER_ERROR_INVALID_EMAIL.JoinWithMessage(
fmt.Sprintf("Invalid email format: %s", email), validationErr)
// Multiple errors can be joined
return nil, UserError_USER_ERROR_VALIDATION_FAILED.Join(
emailErr, passwordErr, ageErr)
The generated error types integrate seamlessly with Sphere’s HTTP server utilities to provide consistent error responses across your entire API.