7 Common Pitfalls When Defining Protobuf Errors in Kratos

Developers using the Kratos framework often fail to annotate protobuf enums with the required errors.code or errors.default_code extensions, use invalid HTTP status codes outside the 1–600 range, or forget to invoke the protoc-gen-go-errors plugin, resulting in silent generation failures or runtime panics.

The go-kratos/kratos microservices framework leverages protobuf definitions to generate strongly-typed error helpers that integrate with both HTTP and gRPC transports. However, the code generation process in cmd/protoc-gen-go-errors imposes strict validation rules that can cause silent failures when protobuf errors in Kratos are defined incorrectly. Understanding the generator's implementation ensures your error enums produce valid, usable Go code.

Common Pitfalls in Error Definitions

1. Forgetting the errors.code or errors.default_code Extensions

Kratos generates Go error helpers from protobuf enums only when the enum values carry the errors.code option or the enum itself carries a errors.default_code. If these extensions are missing, the protoc-gen-go-errors plugin silently skips the file, leaving no generated error types.

According to the source code, the extensions are defined in errors/errors.proto, while the generator reads them in [cmd/protoc-gen-go-errors/errors.go](https://github.com/go-kratos/kratos/blob/main/cmd/protoc-gen-go-errors/errors.go#L62-L78).

Fix:

syntax = "proto3";

import "errors/errors.proto";

enum UserError {
  // Apply a default HTTP code for the entire enum
  option (errors.default_code) = 400;

  // Individual error codes generate separate helper functions
  USER_NOT_FOUND = 0 [(errors.code) = 404];
  INVALID_INPUT = 1 [(errors.code) = 400];
}

2. Using Zero or Out-of-Range Status Codes

Kratos enforces that every error code be greater than 0 and less than or equal to 600 (the valid HTTP range). A value of 0 or anything above 600 triggers a validation panic in the generator during the build phase.

The validation logic resides in [cmd/protoc-gen-go-errors/errors.go](https://github.com/go-kratos/kratos/blob/main/cmd/protoc-gen-go-errors/errors.go#L68-L82).

Fix:

// Correct: 400 is within the valid range
INVALID_INPUT = 0 [(errors.code) = 400];

// Incorrect: triggers a panic
INVALID_INPUT = 0 [(errors.code) = 0];     // Zero is invalid
INVALID_INPUT = 0 [(errors.code) = 700];    // Exceeds 600

3. Relying on Default Code Without Setting It

If an enum defines no per-value errors.code entries, the generator falls back to the enum-level errors.default_code. Omitting both makes the enum invisible to the generation loop, which skips enums when no valid codes are detected (specifically when the processed index remains at 0), as seen in lines 56–60 of [cmd/protoc-gen-go-errors/errors.go](https://github.com/go-kratos/kratos/blob/main/cmd/protoc-gen-go-errors/errors.go#L56-L60).

Fix:

enum AuthError {
  // Required when individual values lack errors.code
  option (errors.default_code) = 401;
  
  UNAUTHORIZED = 0;
  TOKEN_EXPIRED = 1;
}

4. Causing Naming Collisions Across Packages

Generated Go identifiers are formed from the enum name and value name (converted to CamelCase). Two different protobuf packages that define enums with identical names will produce conflicting Go symbols, causing compilation errors.

Fix:

  • Use unique enum names per package (e.g., UserError vs. OrderError).
  • Prefix value names with the domain (e.g., USER_NOT_FOUND, ORDER_NOT_FOUND).

5. Omitting the protoc-gen-go-errors Plugin

Even with correct protobuf definitions, error helpers will not appear unless the plugin is explicitly invoked during code generation. The repository's Makefile contains the build rules for compiling this plugin.

Fix:

Run the generator with the correct plugin flags:

protoc --go_out=. \
       --go-errors_out=. \
       --go-errors_opt=paths=source_relative \
       api/v1/user.proto

6. Ignoring Generated Metadata and Cause Chaining

Kratos errors support operational context via WithMetadata and WithCause. Forgetting these in service implementations strips away debugging information, such as field names or underlying repository errors.

Usage examples are verified in [errors/errors_test.go](https://github.com/go-kratos/kratos/blob/main/errors/errors_test.go#L55-L59).

Correct Usage:

return v1.UserError_INVALID_INPUT("INVALID_INPUT", "missing name").
    WithMetadata(map[string]string{"field": "name"}).
    WithCause(err)

7. Mismatching HTTP and gRPC Codes

When converting between protocols, Kratos maps specific gRPC codes to HTTP status codes. Using custom error codes outside standard mappings can result in unexpected HTTP responses. The mapping logic is defined in [transport/http/status/status.go](https://github.com/go-kratos/kratos/blob/main/transport/http/status/status.go).

Fix:

  • Restrict protobuf error codes to standard HTTP-compatible values (1–600).
  • Use codes.InvalidArgument400 and similar standard mappings for cross-protocol consistency.

Implementation Examples

Protobuf Definition

syntax = "proto3";

package api.v1;

import "errors/errors.proto";

enum UserError {
  option (errors.default_code) = 400;

  USER_NOT_FOUND = 0 [(errors.code) = 404];
  INVALID_INPUT = 1 [(errors.code) = 400];
}

Generated Go Helpers

The plugin generates functions like these in api/v1/user_errors.pb.go:

func UserError_USER_NOT_FOUND(reason, message string) *errors.Error {
    return errors.New(404, reason, message)
}

func UserError_INVALID_INPUT(reason, message string) *errors.Error {
    return errors.New(400, reason, message)
}

Service Implementation

import (
    "github.com/go-kratos/kratos/v2/errors"
    v1 "github.com/yourorg/yourrepo/api/v1"
)

func (s *UserService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserReply, error) {
    user, err := s.repo.Find(req.Id)
    if err != nil {
        return nil, v1.UserError_USER_NOT_FOUND("USER_NOT_FOUND", "user does not exist").
            WithCause(err).
            WithMetadata(map[string]string{"user_id": req.Id})
    }
    return &pb.GetUserReply{User: user}, nil
}

Unit Testing

func TestUserError(t *testing.T) {
    err := v1.UserError_INVALID_INPUT("INVALID_INPUT", "name is required").
        WithMetadata(map[string]string{"field": "name"})
    
    if !errors.IsBadRequest(err) {
        t.Fatalf("expected BadRequest, got %v", err)
    }
    
    if m := errors.Metadata(err); m["field"] != "name" {
        t.Fatalf("expected metadata field 'name'")
    }
}

Summary

  • Always annotate protobuf enums with errors.code or errors.default_code extensions defined in errors/errors.proto.
  • Validate status codes remain between 1 and 600 to avoid generator panics in cmd/protoc-gen-go-errors/errors.go.
  • Invoke the plugin using --go-errors_out when running protoc commands.
  • Prevent naming collisions by using domain-specific enum and value names.
  • Enrich errors with WithMetadata and WithCause to preserve stack context and debugging data.
  • Honor HTTP mappings in transport/http/status/status.go when designing cross-protocol APIs.

Frequently Asked Questions

Why isn't my error enum generating Go helper functions?

The protoc-gen-go-errors plugin silently skips enums that lack the (errors.code) option on values or the (errors.default_code) option on the enum itself. Verify your imports include errors/errors.proto and that at least one of these extensions is present.

What HTTP status codes are valid for Kratos protobuf errors?

Valid codes must be greater than 0 and less than or equal to 600, covering the standard HTTP status code range. The generator panics if it encounters code 0 or values exceeding 600 during validation in cmd/protoc-gen-go-errors/errors.go.

How do I add request context to a generated error?

Use the WithMetadata method to attach key-value pairs and WithCause to wrap underlying errors. These methods are available on *errors.Error return types from generated helpers and are demonstrated in errors/errors_test.go.

Can I use the same enum name in different proto packages?

Avoid duplicate enum names across packages. The generator creates Go functions using the pattern {EnumName}_{VALUE_NAME}, which causes compilation conflicts if identical names exist in different packages within the same Go module.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →