errors

package
v0.0.0-...-2744284 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 17, 2026 License: MIT Imports: 14 Imported by: 0

README

errors

Transport-agnostic error model with HTTP and gRPC adapters.

Use this package when you want one stable error contract across domain, HTTP, and gRPC.

Error Contract

ErrorResponse contains:

  • code - gRPC status code
  • reason - machine-readable error reason
  • message - human-readable message
  • domain - service identifier (e.g., "payment-service")
  • details - additional context (map)
  • violations - field-level validation errors

Quick Reference

Preset gRPC Code HTTP Status Use Case
BadRequest InvalidArgument 400 Validation errors
Unauthorized Unauthenticated 401 Missing auth
Forbidden PermissionDenied 403 Insufficient permissions
NotFound NotFound 404 Resource not found
Conflict AlreadyExists 409 Duplicate resource
RateLimited ResourceExhausted 429 Quota exceeded
Internal Internal 500 Unexpected error
Unavailable Unavailable 503 Service down

Example

Service Layer
package payment

import (
    "context"
    
    ferrors "github.com/vortex-fintech/go-lib/foundation/errors"
)

type Service struct {
    repo Repository
}

func (s *Service) CreatePayment(ctx context.Context, req *CreatePaymentRequest) error {
    // Business validation
    if req.Amount <= 0 {
        return ferrors.ValidationFields(map[string]string{
            "amount": "must be positive",
        })
    }
    
    // Check account exists
    account, err := s.repo.GetAccount(ctx, req.AccountID)
    if err != nil {
        return ferrors.NotFoundID("account", req.AccountID)
    }
    
    // Check KYC status
    if !account.KYCCompleted {
        return ferrors.Precondition("kyc_required", map[string]string{
            "account_id": req.AccountID,
            "status":     account.Status,
        })
    }
    
    // Check for duplicate
    existing, _ := s.repo.GetPaymentByReference(ctx, req.Reference)
    if existing != nil {
        return ferrors.Conflict("reference", req.Reference)
    }
    
    // Process payment...
    return nil
}
HTTP Handler
package api

import (
    "net/http"
    
    ferrors "github.com/vortex-fintech/go-lib/foundation/errors"
)

func (h *Handler) CreatePayment(w http.ResponseWriter, r *http.Request) {
    var req CreatePaymentRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        ferrors.BadRequest("invalid_json").ToHTTP(w)
        return
    }
    
    if err := h.service.CreatePayment(r.Context(), &req); err != nil {
        ferrors.ToErrorResponse(err).ToHTTP(w)
        return
    }
    
    w.WriteHeader(http.StatusCreated)
}
gRPC Service
package grpc

import (
    "context"
    
    ferrors "github.com/vortex-fintech/go-lib/foundation/errors"
)

func (s *Server) CreatePayment(ctx context.Context, req *pb.CreatePaymentRequest) (*pb.Payment, error) {
    if err := s.service.CreatePayment(ctx, fromProto(req)); err != nil {
        return nil, ferrors.ToErrorResponse(err).ToGRPC()
    }
    
    return &pb.Payment{Id: "123"}, nil
}
With Validation
import (
    "github.com/vortex-fintech/go-lib/foundation/errors"
    "github.com/vortex-fintech/go-lib/foundation/validator"
)

func (s *Service) RegisterUser(ctx context.Context, req *RegisterRequest) error {
    // Validate request
    if errs := validator.Validate(req); errs != nil {
        return errors.ValidationFields(errs)
    }
    
    // Business logic...
    return nil
}
Rate Limiting
func (s *Service) CallPartner(ctx context.Context) error {
    if s.rateLimiter.IsExceeded() {
        return ferrors.RateLimited(2 * time.Second)
    }
    
    // Call partner...
    return nil
}
Domain Invariants
import "github.com/vortex-fintech/go-lib/foundation/errors"

func (a *Account) Withdraw(amount int64) error {
    // State invariant
    if a.Status != "active" {
        return errors.StateInvariant(
            nil, 
            "status",
            "account_must_be_active",
        )
    }
    
    // Domain invariant
    if amount > a.Balance {
        return errors.DomainInvariant("amount", "insufficient_funds")
    }
    
    // Transition invariant
    if a.PendingWithdrawal {
        return errors.TransitionInvariant(
            nil,
            "withdrawal",
            "concurrent_withdrawal_not_allowed",
        )
    }
    
    return nil
}
Error Adaptation
func (h *Handler) HandleError(w http.ResponseWriter, err error) {
    // Convert any error to ErrorResponse
    resp := ferrors.ToErrorResponse(err)
    
    // Log with context
    log.Errorw("request failed",
        "code", resp.Code.String(),
        "reason", resp.Reason,
        "message", resp.Message,
    )
    
    // Send to client
    resp.ToHTTP(w)
}

Business Examples

Payment Flow
// 1. Validation
if amount <= 0 {
    return ferrors.ValidationFields(map[string]string{
        "amount": "must_be_positive",
    })
}

// 2. Not found
account, err := repo.GetAccount(ctx, accountID)
if err != nil {
    return ferrors.NotFoundID("account", accountID)
}

// 3. Business rule
if !account.KYCCompleted {
    return ferrors.Precondition("kyc_required", map[string]string{
        "account_id": accountID,
    })
}

// 4. Conflict
if exists := repo.GetByReference(ctx, ref); exists != nil {
    return ferrors.Conflict("reference", ref)
}

// 5. Rate limit
if limiter.Exceeded() {
    return ferrors.RateLimited(5 * time.Second)
}

// 6. Success
return nil
Auth Flow
// Missing token
if token == "" {
    return ferrors.Unauthorized("bearer", "api")
}

// Invalid token
if !ValidateToken(token) {
    return ferrors.Unauthenticated().WithReason("invalid_token")
}

// Insufficient permissions
if !user.HasPermission("payment:write") {
    return ferrors.Forbidden("payment:write", "payments")
}

HTTP Mapping

gRPC Code HTTP Status
Canceled 499
InvalidArgument 400
DeadlineExceeded 504
NotFound 404
AlreadyExists 409
PermissionDenied 403
ResourceExhausted 429
FailedPrecondition 412
Aborted 409
OutOfRange 400
Unimplemented 501
Internal 500
Unavailable 503
DataLoss 500
Unauthenticated 401

Best Practices

  1. Use presets - Don't create ErrorResponse directly
  2. Add reason - Always set machine-readable reason
  3. Use details - Add context, never PII
  4. Use violations - For field-level validation errors
  5. Set domain - For cross-service analytics

Testing

go test ./foundation/errors/... -cover
go vet ./foundation/errors/...

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DomainInvariant

func DomainInvariant(field, reason string) error

func GRPCRateLimited

func GRPCRateLimited(retryAfter time.Duration) error

GRPCRateLimited returns a ready gRPC error with RetryInfo + ErrorInfo(reason).

func HTTPStatus

func HTTPStatus(code codes.Code) int

func IsInvariant

func IsInvariant(err error) bool

func StateInvariant

func StateInvariant(base error, field, reason string) error

func TransitionInvariant

func TransitionInvariant(base error, field, reason string) error

Types

type ErrorResponse

type ErrorResponse struct {
	Code       codes.Code        `json:"code"`
	Reason     Reason            `json:"reason,omitempty"`
	Domain     string            `json:"domain,omitempty"` // optional service domain (for example "auth-service")
	Message    string            `json:"message"`
	Details    map[string]string `json:"details,omitempty"`
	Violations []FieldViolation  `json:"violations,omitempty"`
}

func Aborted

func Aborted() ErrorResponse

func AlreadyExists

func AlreadyExists() ErrorResponse

func Canceled

func Canceled() ErrorResponse

func Conflict

func Conflict(field, value string) ErrorResponse

Conflict(field,value) -> 409/AlreadyExists.

func DataLoss

func DataLoss() ErrorResponse

func DeadlineExceeded

func DeadlineExceeded() ErrorResponse

func FailedPrecondition

func FailedPrecondition() ErrorResponse

func Forbidden

func Forbidden(action, resource string) ErrorResponse

Forbidden for RBAC checks.

func FromGRPC

func FromGRPC(err error) ErrorResponse

func FromPlayground

func FromPlayground(err play.ValidationErrors, tagToReason map[string]string) ErrorResponse

FromPlayground adapts go-playground/validator errors into InvalidArgument + Violations. It attempts to resolve nested field path using StructNamespace() and falls back to Namespace() without root type.

func Internal

func Internal() ErrorResponse

func InvalidArgument

func InvalidArgument() ErrorResponse

func New

func New(message string, code codes.Code, details map[string]string) ErrorResponse

func Newf

func Newf(code codes.Code, reason, format string, a ...any) ErrorResponse

Newf is a formatted constructor with reason.

func NotFound

func NotFound() ErrorResponse

func NotFoundID

func NotFoundID(resource, id string) ErrorResponse

NotFoundID(resource,id).

func NotFoundWith

func NotFoundWith(resourceKey, value string) ErrorResponse

func OutOfRange

func OutOfRange() ErrorResponse

func PermissionDenied

func PermissionDenied() ErrorResponse

func Precondition

func Precondition(reason string, details map[string]string) ErrorResponse

Precondition(reason, details) -> 412/FailedPrecondition.

func RateLimited

func RateLimited(retryAfter time.Duration) ErrorResponse

RateLimited with machine-friendly retry delay in milliseconds.

func ResourceExhausted

func ResourceExhausted() ErrorResponse

func To

func To(code codes.Code, reason, msg string) ErrorResponse

func ToErrorResponse

func ToErrorResponse(err error) ErrorResponse

ToErrorResponse converts any error into ErrorResponse (transport-agnostic). Supported inputs: - ErrorResponse / *ErrorResponse (direct passthrough) - context.Canceled / context.DeadlineExceeded - InvariantError (DomainInvariant/StateInvariant/TransitionInvariant)

func ToValidation

func ToValidation(field, reason string) ErrorResponse

Convenience helpers.

func Unauthenticated

func Unauthenticated() ErrorResponse

func Unauthorized

func Unauthorized(scheme, realm string) ErrorResponse

Unauthorized with optional client hints.

func Unavailable

func Unavailable() ErrorResponse

func Unimplemented

func Unimplemented() ErrorResponse

func Unknown

func Unknown() ErrorResponse

Preset factories (immutable templates).

func Unsupported

func Unsupported(name, value string) ErrorResponse

func ValidationFields

func ValidationFields(fields map[string]string) ErrorResponse

Fast constructors for common cases.

func ValidationViolations

func ValidationViolations(v []FieldViolation) ErrorResponse

func (ErrorResponse) Error

func (e ErrorResponse) Error() string

func (ErrorResponse) ToGRPC

func (e ErrorResponse) ToGRPC() error

func (ErrorResponse) ToHTTP

func (e ErrorResponse) ToHTTP(w http.ResponseWriter)

func (ErrorResponse) ToHTTPWithRetry

func (e ErrorResponse) ToHTTPWithRetry(w http.ResponseWriter, retryAfter time.Duration)

ToHTTPWithRetry sets Retry-After (seconds) and writes the error body.

func (ErrorResponse) ToString

func (e ErrorResponse) ToString() string

func (ErrorResponse) WithDetail

func (e ErrorResponse) WithDetail(k, v string) ErrorResponse

func (ErrorResponse) WithDetails

func (e ErrorResponse) WithDetails(m map[string]string) ErrorResponse

func (ErrorResponse) WithDomain

func (e ErrorResponse) WithDomain(d string) ErrorResponse

func (ErrorResponse) WithReason

func (e ErrorResponse) WithReason(r string) ErrorResponse

func (ErrorResponse) WithViolations

func (e ErrorResponse) WithViolations(v []FieldViolation) ErrorResponse

type FieldViolation

type FieldViolation struct {
	Field       string `json:"field"`
	Reason      string `json:"reason,omitempty"`
	Description string `json:"description,omitempty"`
}

func ViolationsFromMap

func ViolationsFromMap(m map[string]string) []FieldViolation

type InvariantError

type InvariantError struct {
	Kind   InvariantKind
	Base   error
	Field  string
	Reason string
}

InvariantError is a unified type for field/state/transition invariant failures.

func (InvariantError) Error

func (e InvariantError) Error() string

func (InvariantError) Unwrap

func (e InvariantError) Unwrap() error

Unwrap enables errors.Is / errors.As.

type InvariantKind

type InvariantKind string

InvariantKind classifies domain invariant failures.

const (
	KindDomain     InvariantKind = "domain"
	KindState      InvariantKind = "state"
	KindTransition InvariantKind = "transition"
)

type Reason

type Reason string

Reason is a stable machine-readable code.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL