Skip to content
protoc-gen-sphere-errors

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 reason in Error() when set; otherwise uses the enum value name
  • Provides Join and JoinWithMessage methods for error composition via httpx.NewError
  • Supports default status codes for enum types
  • Deduplicates allow_alias enum values so generated switches compile
  • Skips the zero-value *_UNSPECIFIED entry as a returned business error in generated helpers

Configuration Parameters

  • version: Print the plugin version and exit. (Default: false)
  • new_errors_func: Constructor used by Join / 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@latest

Prerequisites

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/errors

Usage 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_relative

Proto 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 — implements error. Returns reason when set, otherwise the enum value name.
  • GetCode() int32 — the numeric enum value (e.g. 1001)
  • GetStatus() int32 — the HTTP status code
  • GetMessage() string — the user-facing default message
  • Join(errs ...error) error — wraps causes with httpx.NewError
  • JoinWithMessage(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

  1. Use meaningful error codes: Choose enum values that clearly indicate the error type
  2. Set appropriate HTTP status codes: Use standard HTTP status codes (400, 401, 403, 404, 500, etc.)
  3. Provide clear messages: Write user-friendly error messages that can be displayed to end users
  4. Use reasons for programmatic handling: Include reason strings that client applications can use for conditional logic
  5. Group related errors: Keep related errors in the same enum for better organization
  6. Preserve context with Join: Always use .Join() to wrap underlying errors for better debugging

Integration Notes

  • httpz.WithJson maps these errors to structured JSON with the correct HTTP status
  • Pair with httpz.SetDefaultErrorParser if you need to merge validation, not-found, or domain errors
  • The generated errors implement httpx.Error interfaces so the runtime can read status, code, and message without extra wrapping
Last updated on