fail

package module
v3.2.0 Latest Latest
Warning

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

Go to latest
Published: Feb 3, 2026 License: MIT Imports: 13 Imported by: 0

README

🔥 FAIL - Failure Abstraction & Instrumentation Layer

Production-grade, type-safe error handling for Go with explicit numbering, validation, and rich metadata.

FAIL provides a revolutionary approach to error management with explicitly numbered IDs, automatic validation, localization support, and beautiful ergonomics.


✨ Why FAIL?

🎯 Explicitly Numbered, Stable IDs

Error IDs use explicit numbering that's stable across versions:

// Define errors with explicit numbers - they NEVER change!
var (
    AuthInvalidCredentials = fail.ID(0, "AUTH", 0, true, "AuthInvalidCredentials")  // 0_AUTH_0000_S
    AuthTokenExpired       = fail.ID(0, "AUTH", 1, true, "AuthTokenExpired")        // 0_AUTH_0001_S
    UserNotFound           = fail.ID(0, "USER", 0, true, "UserNotFound")            // 0_USER_0000_S
)

// Numbers are explicitly assigned and validated
// No surprises, no auto-generation, complete control 🎯
🛡️ Built-in Validation

FAIL validates IDs at package initialization time:

// ✅ Valid - name starts with domain, sequential numbering
var Good = fail.ID(0, "AUTH", 0, true, "AuthInvalidCredentials")

// ❌ PANIC - name doesn't start with domain  
var Bad1 = fail.ID(0, "AUTH", 1, true, "InvalidCredentials")

// ❌ PANIC - duplicate number
var Bad2 = fail.ID(0, "AUTH", 0, true, "AuthDuplicateNumber")

// ❌ PANIC - too similar name (Levenshtein distance < 3)
var Bad3 = fail.ID(0, "AUTH", 2, true, "AuthInvalidCredential")

// ❌ PANIC - gap in numbering (skipped from 0 to 2)
var Bad4 = fail.ID(0, "AUTH", 2, true, "AuthSkippedNumber")
🌍 First-Class Localization

Built-in support for multi-language applications:

var ErrUserNotFound = fail.Form(UserNotFound, "user %s not found", false, nil).
    AddLocalizations(map[string]string{
        "pt-BR": "usuário %s não encontrado",
        "es-ES": "usuario %s no encontrado",
        "fr-FR": "utilisateur %s introuvable",
    })

// Use with automatic rendering
err := fail.New(UserNotFound).
    WithLocale("pt-BR").
    WithArgs("alice@example.com")

// err.Error() outputs: [0_USER_0000_S] usuário alice@example.com não encontrado
🔗 Fluent Builder API

Chain methods for expressive error construction:

err := fail.New(UserValidationFailed).
    WithLocale("en-US").
    WithArgs("admin").
    Msg("custom message override").
    With(cause).
    Internal("debug: validation failed on email field").
    Trace("step 1: email validation").
    Validation("email", "invalid format").
    AddMeta("request_id", "abc-123").
    LogAndRecord()

🚀 Quick Start

1. Install
go get github.com/MintzyG/fail/v3
2. Define Error IDs (Centralized)

IMPORTANT: Define all your error IDs in one package to ensure proper sequential validation.

Create errors/errors.go:

package errors

import "github.com/MintzyG/fail/v3"

// Auth domain errors - names MUST start with "Auth"
var (
    AuthInvalidCredentials = fail.ID(0, "AUTH", 0, true, "AuthInvalidCredentials")
    AuthTokenExpired       = fail.ID(0, "AUTH", 1, true, "AuthTokenExpired")
    AuthValidationFailed   = fail.ID(0, "AUTH", 0, false, "AuthValidationFailed") // Dynamic!
)

// User domain errors - names MUST start with "User"
var (
    UserNotFound      = fail.ID(0, "USER", 0, true, "UserNotFound")
    UserAlreadyExists = fail.ID(0, "USER", 1, true, "UserAlreadyExists")
)

// Database domain errors
var (
    DatabaseConnectionFailed = fail.ID(1, "DATABASE", 0, true, "DatabaseConnectionFailed")
    DatabaseQueryTimeout     = fail.ID(1, "DATABASE", 1, true, "DatabaseQueryTimeout")
)
// Option A: Simple registration
func init() {
    fail.Register(fail.ErrorDefinition{
        ID:             UserNotFound,
        DefaultMessage: "user not found",
        IsSystem:       false,
    })
}

// Option B: Form() - one-liner with sentinel + localization
var ErrUserNotFound = fail.Form(
    UserNotFound, 
    "user %s not found", 
    false, 
    nil,
).AddLocalizations(map[string]string{
    "pt-BR": "usuário %s não encontrado",
    "es-ES": "usuario %s no encontrado",
})
4. Use Everywhere
func GetUser(email string) (*User, error) {
    user, err := db.GetUser(email)
    if err != nil {
        return nil, fail.New(UserNotFound).
            WithArgs(email).
            With(err). // Wrap underlying error
            Internal(fmt.Sprintf("database query failed for %s", email))
    }
    return user, nil
}

func HandleRequest(w http.ResponseWriter, r *http.Request) {
    user, err := GetUser("alice@example.com")
    if err != nil {
        // err.Error() automatically localizes and renders
        log.Println(err.Error())
        // Output: [0_USER_0000_S] user alice@example.com not found
    }
}

📖 Core Concepts

Error ID Structure

Format: LEVEL_DOMAIN_NUMBER_TYPE

  • LEVEL: Severity level (0-9, where 0 = info, 9 = critical)
  • DOMAIN: Error category (e.g., AUTH, USER, DATABASE)
  • NUMBER: Sequential number within domain+type (0000-9999)
  • TYPE: S (Static - message won't change) or D (Dynamic - message varies)

Examples:

  • 0_AUTH_0000_S - Low severity, Auth domain, first static error
  • 1_DATABASE_0001_S - Medium severity, Database domain, second static error
  • 0_USER_0000_D - Low severity, User domain, first dynamic error
Static vs Dynamic Errors

Static Errors - Message is the same every time:

var AuthTokenExpired = fail.ID(0, "AUTH", 1, true, "AuthTokenExpired")
// Always: "token expired"

Dynamic Errors - Message varies per occurrence:

var UserValidationFailed = fail.ID(0, "USER", 0, false, "UserValidationFailed")
// Varies: "validation failed: email invalid", "validation failed: name too short", etc.
Sequential Numbering

Within each DOMAIN + TYPE combination, numbers must be sequential starting from 0:

// ✅ Correct - sequential within AUTH static
var (
    AuthError1 = fail.ID(0, "AUTH", 0, true, "AuthError1")  // 0_AUTH_0000_S
    AuthError2 = fail.ID(0, "AUTH", 1, true, "AuthError2")  // 0_AUTH_0001_S
    AuthError3 = fail.ID(0, "AUTH", 2, true, "AuthError3")  // 0_AUTH_0002_S
)

// ✅ Correct - AUTH dynamic has separate sequence
var (
    AuthDynamic1 = fail.ID(0, "AUTH", 0, false, "AuthDynamic1")  // 0_AUTH_0000_D
    AuthDynamic2 = fail.ID(0, "AUTH", 1, false, "AuthDynamic2")  // 0_AUTH_0001_D
)

// ❌ PANIC - gap in numbering (skipped number 1)
var (
    AuthError1 = fail.ID(0, "AUTH", 0, true, "AuthError1")
    AuthError3 = fail.ID(0, "AUTH", 2, true, "AuthError3")  // Missing number 1!
)

🎨 Features

🌍 Localization & Rendering

FAIL provides first-class support for multi-language applications.

Basic Localization
// Set global default locale
fail.SetDefaultLocale("en-US")

// Add translations
var ErrUserNotFound = fail.Form(UserNotFound, "user %s not found", false, nil).
    AddLocalizations(map[string]string{
        "pt-BR": "usuário %s não encontrado",
        "es-ES": "usuario %s no encontrado",
        "fr-FR": "utilisateur %s introuvable",
    })

// Use per-error locale
err := fail.New(UserNotFound).
    WithLocale("fr-FR").
    WithArgs("bob@example.com")

msg := err.GetRendered()  // "utilisateur bob@example.com introuvable"
Template Rendering

Use standard fmt placeholders:

err := fail.New(UserNotFound).
    WithArgs("alice@example.com").
    Render()  // Formats template with args

// Or get rendered message directly
msg := err.GetRendered()
Localization Plugin

For advanced use cases, implement the Localizer interface:

import "github.com/MintzyG/fail/v3/plugins/localization"

localizer := localization.New()
fail.SetLocalizer(localizer)

// Bulk register translations
fail.RegisterLocalizations("pt-BR", map[fail.ErrorID]string{
    UserNotFound:      "usuário %s não encontrado",
    AuthTokenExpired:  "token expirado",
})
🔄 Robust Retry Logic

Built-in retry mechanism with configurable backoff strategies.

// Basic retry (uses global config)
err := fail.Retry(func() error {
    return db.Connect()
})

// Configure global retry behavior
fail.SetRetryConfig(&fail.RetryConfig{
    MaxAttempts: 5,
    ShouldRetry: fail.IsRetryableDefault,
    Delay: fail.BackoffExponential(100 * time.Millisecond),
})

// Advanced retry with custom config
cfg := fail.RetryConfig{
    MaxAttempts: 3,
    Delay: fail.WithJitter(
        fail.BackoffExponential(100*time.Millisecond), 
        0.3, // 30% jitter
    ),
    ShouldRetry: func(err error) bool {
        return fail.IsSystem(err)
    },
}

err := fail.RetryCFG(cfg, func() error {
    return remoteAPI.Call()
})

// Retry with return value
user, err := fail.RetryValue(func() (*User, error) {
    return repo.GetUser(id)
})
Backoff Strategies
// Constant backoff
fail.BackoffConstant(500 * time.Millisecond)

// Linear backoff
fail.BackoffLinear(200 * time.Millisecond)

// Exponential backoff
fail.BackoffExponential(100 * time.Millisecond)

// With jitter (recommended for distributed systems)
fail.WithJitter(
    fail.BackoffExponential(100 * time.Millisecond),
    0.3, // 30% jitter
)
Marking Errors as Retryable
err := fail.New(DatabaseTimeout).
    AddMeta("retryable", true)

// Will be retried by default ShouldRetry function
fail.Retry(func() error {
    return err
})
🔗 Error Chaining

Fluent chain API for executing steps with automatic error handling.

err := fail.Chain(validateRequest).
    Then(checkPermissions).
    ThenCtx("database", saveData).       // Adds context to error
    ThenIf(shouldNotify, sendEmail).     // Conditional execution
    OnError(func(e *fail.Error) {
        log.Error("chain failed", e)     // Side effects on error
    }).
    Catch(func(e *fail.Error) *fail.Error {
        // Transform error
        return e.AddMeta("caught", true)
    }).
    Finally(func() {
        cleanup()                         // Always executes
    }).
    Error()  // Returns *fail.Error or nil

// Check chain status
if err != nil {
    step := err.Error().Step()  // Get number of successful steps
}
📦 Error Groups

Collect multiple errors thread-safely (perfect for parallel validation).

group := fail.NewErrorGroup(10)

// Add errors safely from goroutines
var wg sync.WaitGroup
for _, item := range items {
    wg.Add(1)
    go func(i Item) {
        defer wg.Done()
        if err := validate(i); err != nil {
            group.Add(err)
        }
    }(item)
}
wg.Wait()

// Convert to single error
if group.HasErrors() {
    return group.ToError()  // Returns MultipleErrors with all errors in metadata
}

// Access individual errors
for _, err := range group.Errors() {
    log.Println(err)
}
🪝 Hooks & Lifecycle Events

Hook into error lifecycle events for monitoring, logging, or metrics.

// Global hooks
fail.OnCreate(func(e *fail.Error, data map[string]any) {
    metrics.Increment("errors.created", map[string]string{
        "domain": e.ID.Domain(),
    })
})

fail.OnLog(func(e *fail.Error, data map[string]any) {
    // Called when e.Log() is used
})

fail.OnTrace(func(e *fail.Error, data map[string]any) {
    // Called when e.Record() is used
})

fail.OnWrap(func(wrapper *fail.Error, wrapped error) {
    // Called when .With() is used
})

fail.OnMatch(func(e *fail.Error, data map[string]any) {
    // Called when error is matched in fail.Match()
})

// Available hooks:
// - HookCreate: When fail.New() is called
// - HookLog: When .Log() or .LogCtx() is called
// - HookTrace: When .Record() or .RecordCtx() is called
// - HookWrap: When .With() wraps another error
// - HookFromSuccess: When fail.From() successfully maps an error
// - HookFromFail: When fail.From() fails to map an error
// - HookForm: When fail.Form() creates a sentinel
// - HookTranslate: When error is translated
// - HookMatch: When error matches in pattern matching
📊 Observability

Integrate with your logging and tracing infrastructure.

Logging
// Implement the Logger interface
type MyLogger struct {
    logger *zap.Logger
}

func (l *MyLogger) Log(err *fail.Error) {
    l.logger.Error("error occurred",
        zap.String("id", err.ID.String()),
        zap.String("domain", err.ID.Domain()),
        zap.String("message", err.GetRendered()),
        zap.Bool("is_system", err.IsSystem),
    )
}

func (l *MyLogger) LogCtx(ctx context.Context, err *fail.Error) {
    // Extract context values, trace IDs, etc.
    l.logger.Error("error occurred", 
        zap.String("trace_id", getTraceID(ctx)),
        zap.String("id", err.ID.String()),
    )
}

// Register your logger
fail.SetLogger(&MyLogger{logger: zapLogger})

// Use in code
err := fail.New(AuthTokenExpired).Log()  // Automatically logged
Tracing
// Implement the Tracer interface
type MyTracer struct {
    tracer trace.Tracer
}

func (t *MyTracer) Record(err *fail.Error) *fail.Error {
    span := t.tracer.StartSpan("error")
    defer span.End()
    
    span.SetAttributes(
        attribute.String("error.id", err.ID.String()),
        attribute.String("error.domain", err.ID.Domain()),
    )
    return err
}

func (t *MyTracer) RecordCtx(ctx context.Context, err *fail.Error) *fail.Error {
    span := trace.SpanFromContext(ctx)
    span.RecordError(err)
    return err
}

// Register your tracer
fail.SetTracer(&MyTracer{tracer: otelTracer})

// Use in code
err := fail.New(DatabaseTimeout).Record()  // Automatically traced
OpenTelemetry Plugin

FAIL includes an OpenTelemetry plugin for easy integration:

import "github.com/MintzyG/fail/v3/plugins/otel"

// Create OpenTelemetry tracer with configuration
tracer := otel.New(
    otel.WithMode(otel.RecordSmart),       // Smart mode: events for domain, status for system
    otel.WithStackTrace(),                 // Include stack traces
    otel.WithAttributePrefix("app.error"), // Custom attribute prefix
)

fail.SetTracer(tracer)

// Errors are automatically recorded in spans
err := fail.New(DatabaseTimeout).RecordCtx(ctx)
🔄 Generic Error Mapping

Map external library errors to your domain errors automatically.

// Implement the Mapper interface
type PostgresMapper struct{}

func (m *PostgresMapper) Name() string {
    return "postgres"
}

func (m *PostgresMapper) Priority() int {
    return 100  // Higher priority = checked first
}

func (m *PostgresMapper) Map(err error) (*fail.Error, bool) {
    var pgErr *pgconn.PgError
    if errors.As(err, &pgErr) {
        switch pgErr.Code {
        case "23505": // unique_violation
            return fail.New(UserAlreadyExists), true
        case "23503": // foreign_key_violation
            return fail.New(DatabaseForeignKeyViolation), true
        }
    }
    return nil, false
}

// Register mapper
fail.RegisterMapper(&PostgresMapper{})

// Use automatically
dbErr := db.Insert(user)
return fail.From(dbErr)  // Automatically mapped to UserAlreadyExists!
🔀 Error Translation

Convert FAIL errors to other formats (HTTP responses, gRPC status, CLI output).

// Implement the Translator interface
type HTTPTranslator struct{}

func (t *HTTPTranslator) Name() string {
    return "http"
}

func (t *HTTPTranslator) Supports(err *fail.Error) error {
    // Check if error can be translated
    return nil
}

func (t *HTTPTranslator) Translate(err *fail.Error) (any, error) {
    statusCode := 500
    if !err.IsSystem {
        statusCode = 400
    }
    
    return map[string]any{
        "error": map[string]any{
            "code":    err.ID.String(),
            "message": err.GetRendered(),
            "domain":  err.ID.Domain(),
        },
        "status": statusCode,
    }, nil
}

// Register translator
fail.RegisterTranslator(&HTTPTranslator{})

// Use in HTTP handlers
func handler(w http.ResponseWriter, r *http.Request) {
    user, err := getUser(r.Context())
    if err != nil {
        if failErr, ok := fail.As(err); ok {
            resp, _ := fail.To(failErr, "http")
            json.NewEncoder(w).Encode(resp)
            return
        }
    }
}

// Type-safe translation
resp, err := fail.ToAs[HTTPResponse](failErr, "http")
🎯 Pattern Matching

Match errors elegantly without nested if-statements.

fail.Match(err).
    Case(AuthInvalidCredentials, func(e *fail.Error) {
        log.Info("invalid credentials attempt")
    }).
    CaseAny(func(e *fail.Error) {
        log.Warn("authentication error")
    }, AuthTokenExpired, AuthSessionExpired).
    CaseSystem(func(e *fail.Error) {
        alert.PagerDuty(e)  // Alert on-call for system errors
    }).
    CaseDomain(func(e *fail.Error) {
        // Handle expected business logic errors
    }).
    Default(func(err error) {
        // Handle unknown errors
        log.Error("unexpected error", err)
    })
🛠️ Helper Functions
// Quick constructors
err := fail.Fast(AuthTokenExpired, "custom message")
err := fail.Wrap(DatabaseQueryFailed, dbErr)
err := fail.WrapMsg(DatabaseQueryFailed, "query failed for user", dbErr)
err := fail.FromWithMsg(genericErr, "additional context")

// Panic helpers (for initialization)
fail.Must(err)
fail.MustNew(AuthTokenExpired)

// Type checking
if fail.Is(err, AuthTokenExpired) {
    // Handle specifically
}

if fail.IsSystem(err) {
    // System/infrastructure error
}

if fail.IsDomain(err) {
    // Expected business logic error
}

// Extract information
if failErr, ok := fail.As(err); ok {
    id := failErr.ID
    msg := fail.GetMessage(err)
    internal := fail.GetInternalMessage(err)
    validations, _ := fail.GetValidations(err)
}

// Metadata helpers
meta, exists := fail.GetMeta(err, "request_id")
traces, ok := fail.GetTraces(err)
debug, ok := fail.GetDebug(err)
🗂️ Metadata & Context

Attach rich metadata to errors:

err := fail.New(UserValidationFailed).
    AddMeta("request_id", "abc-123").
    AddMeta("user_ip", "192.168.1.1").
    Validation("email", "invalid format").
    Validation("password", "too weak").
    Trace("step 1: validation").
    Debug("validation context: signup form")

// Extract metadata
if validations, ok := fail.GetValidations(err); ok {
    for _, v := range validations {
        fmt.Printf("Field %s: %s\n", v.Field, v.Message)
    }
}

🏗️ Advanced Usage

Custom Registries

Create isolated registries for testing or multi-tenant applications:

// Create custom registry
registry := fail.MustNewRegistry("tenant-a")

// Register errors to custom registry
registry.Register(&fail.Error{
    ID:       TenantSpecificError,
    Message:  "tenant specific error",
    IsSystem: false,
})

// Use custom registry
err := registry.New(TenantSpecificError)

// Check which registry an error belongs to
if err.FromRegistry(registry) {
    // Handle tenant-specific error
}
ID Validation
// Validate all IDs after initialization (optional but recommended)
func main() {
    fail.ValidateIDs()  // Panics if gaps or duplicates found
    
    // Your application code
}
Export Error Catalog

Generate documentation from your errors:

// Export all registered error IDs as JSON
data, err := fail.ExportIDList()
if err != nil {
    log.Fatal(err)
}

// Write to file for documentation
os.WriteFile("errors.json", data, 0644)

// Output format:
// [
//   {
//     "name": "AuthInvalidCredentials",
//     "domain": "AUTH",
//     "static": true,
//     "level": 0,
//     "number": 0,
//     "id": "0_AUTH_0000_S"
//   },
//   ...
// ]
Configuration
// Allow internal library logging (useful for debugging)
fail.AllowInternalLogs(true)

// Control static error mutation behavior
// Default: mutations silently fail
fail.AllowStaticMutations(false, false)

// Panic on static mutation attempts (strict mode)
fail.AllowStaticMutations(false, true)

// Allow mutations (not recommended)
fail.AllowStaticMutations(true, false)

// Control runtime panics
fail.AllowRuntimePanics(true)  // Panic on programming errors

🎓 Best Practices

1. Centralize Error Definitions

Define all error IDs in a single package:

// errors/errors.go
package errors

import "github.com/MintzyG/fail/v3"

// All application error IDs
var (
    // Auth domain
    AuthInvalidCredentials = fail.ID(0, "AUTH", 0, true, "AuthInvalidCredentials")
    AuthTokenExpired       = fail.ID(0, "AUTH", 1, true, "AuthTokenExpired")
    
    // User domain
    UserNotFound      = fail.ID(0, "USER", 0, true, "UserNotFound")
    UserAlreadyExists = fail.ID(0, "USER", 1, true, "UserAlreadyExists")
)
2. Use Static for Predictable Messages
// ✅ Good - message is always the same
var AuthTokenExpired = fail.ID(0, "AUTH", 1, true, "AuthTokenExpired")

// ❌ Bad - message varies, should be dynamic
var UserValidationFailed = fail.ID(0, "USER", 0, true, "UserValidationFailed")

// ✅ Good - dynamic for varying messages
var UserValidationFailed = fail.ID(0, "USER", 0, false, "UserValidationFailed")
3. Register with Localization
var ErrUserNotFound = fail.Form(
    UserNotFound,
    "user %s not found",
    false,
    nil,
).AddLocalizations(map[string]string{
    "es-ES": "usuario %s no encontrado",
    "pt-BR": "usuário %s não encontrado",
})
4. Use Appropriate Severity Levels
// Level 0: Info/expected errors
var UserNotFound = fail.ID(0, "USER", 0, true, "UserNotFound")

// Level 1: Warnings
var RateLimitExceeded = fail.ID(1, "RATE", 0, true, "RateLimitExceeded")

// Level 2-3: Errors
var DatabaseTimeout = fail.ID(2, "DATABASE", 0, true, "DatabaseTimeout")

// Level 4-5: Critical
var DatabaseConnectionLost = fail.ID(4, "DATABASE", 1, true, "DatabaseConnectionLost")
5. Wrap External Errors
user, err := db.GetUser(id)
if err != nil {
    return nil, fail.New(DatabaseQueryFailed).
        With(err).  // Preserve original error
        Internal(fmt.Sprintf("failed to get user %s", id)).
        AddMeta("user_id", id)
}
6. Add Context Early
if err := validateEmail(email); err != nil {
    return fail.From(err).
        AddMeta("email", email).
        AddMeta("request_id", requestID).
        Trace("email validation")
}

🔌 Plugins

FAIL supports optional plugins for enhanced functionality:

Localization Plugin
import "github.com/MintzyG/fail/v3/plugins/localization"

localizer := localization.New()
fail.SetLocalizer(localizer)
OpenTelemetry Plugin
import "github.com/MintzyG/fail/v3/plugins/otel"

tracer := otel.New(
    otel.WithMode(otel.RecordSmart),
    otel.WithStackTrace(),
)
fail.SetTracer(tracer)

📚 Examples

Complete Example: User Service
package main

import (
    "context"
    "github.com/MintzyG/fail/v3"
)

// Define error IDs
var (
    UserNotFound      = fail.ID(0, "USER", 0, true, "UserNotFound")
    UserAlreadyExists = fail.ID(0, "USER", 1, true, "UserAlreadyExists")
    UserValidation    = fail.ID(0, "USER", 0, false, "UserValidation")
    DatabaseError     = fail.ID(2, "DATABASE", 0, true, "DatabaseError")
)

// Register with localization
var (
    ErrUserNotFound = fail.Form(UserNotFound, "user %s not found", false, nil).
        AddLocalizations(map[string]string{
            "pt-BR": "usuário %s não encontrado",
        })
)

type UserService struct {
    db *Database
}

func (s *UserService) CreateUser(ctx context.Context, email string) error {
    // Validate input
    if err := validateEmail(email); err != nil {
        return fail.New(UserValidation).
            Validation("email", "invalid format").
            With(err)
    }
    
    // Check if exists
    existing, err := s.db.GetUser(ctx, email)
    if err == nil {
        return fail.New(UserAlreadyExists).
            WithArgs(email).
            AddMeta("existing_id", existing.ID)
    }
    
    // Create user
    if err := s.db.Insert(ctx, email); err != nil {
        return fail.From(err).  // Maps DB errors automatically
            Internal(fmt.Sprintf("failed to insert user %s", email)).
            AddMeta("email", email).
            LogAndRecordCtx(ctx)
    }
    
    return nil
}

func main() {
    // Setup
    fail.SetDefaultLocale("en-US")
    fail.SetLogger(myLogger)
    fail.SetTracer(myTracer)
    fail.RegisterMapper(&PostgresMapper{})
    
    // Validate IDs
    fail.ValidateIDs()
    
    // Run application
    service := &UserService{db: db}
    if err := service.CreateUser(ctx, "test@example.com"); err != nil {
        fail.Match(err).
            Case(UserAlreadyExists, func(e *fail.Error) {
                log.Info("user already exists")
            }).
            CaseSystem(func(e *fail.Error) {
                log.Error("system error", e)
            }).
            Default(func(err error) {
                log.Error("unexpected error", err)
            })
    }
}

🤝 Contributing

Contributions are welcome! Please open an issue or PR.


📄 License

MIT License


FAIL - Because production-grade error handling shouldn't be a failure! 🔥

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	UnregisteredError           = internalID(0, 0, false, "FailUnregisteredError")
	TranslateUnregisteredError  = internalID(0, 1, false, "FailTranslateUnregisteredError")
	TranslatorNotFound          = internalID(0, 2, false, "FailTranslatorNotFound")
	TranslateUnsupportedError   = internalID(0, 3, false, "FailTranslateUnsupportedError")
	TranslatePanicked           = internalID(0, 4, false, "FailTranslatorPanicked")
	TranslateWrongType          = internalID(0, 5, false, "FailTranslateWrongType")
	MultipleErrors              = internalID(0, 6, false, "FailMultipleErrors")
	UnknownError                = internalID(0, 7, false, "FailUnknownError")
	NotMatchedInAnyMapper       = internalID(0, 8, false, "FailNotMatchedInAnyMapper")
	NoMapperRegistered          = internalID(0, 9, false, "FailNoMapperRegistered")
	TranslatorAlreadyRegistered = internalID(0, 10, false, "FailTranslatorAlreadyRegistered")
	RuntimeIDInvalid            = internalID(9, 11, false, "FailRuntimeIDInvalid")
	UnregisteredIDError         = internalID(9, 12, false, "FailIDNotRegisteredError")
	RegisterManyError           = internalID(9, 13, false, "FailRegisterManyError")
	RegistryAlreadyRegistered   = internalID(9, 14, false, "FailRegistryAlreadyRegistered")

	TranslatorNil       = internalID(0, 0, true, "FailTranslatorNil")
	TranslatorNameEmpty = internalID(0, 1, true, "FailTranslatorNameEmpty")
)

IDs

Functions

func AllowInternalLogs

func AllowInternalLogs(allow bool)

AllowInternalLogs enables or disables internal library logging. When enabled, the library will log warnings and debug information about error processing, such as:

  • Double calls to From()
  • Unmapped errors
  • Mapper registration issues

This is useful for debugging integration issues but should be disabled in production to avoid log spam. Default is false.

Example:

fail.AllowInternalLogs(true)  // Enable logging
fail.AllowInternalLogs(false) // Disable logging (default)

func AllowRuntimePanics

func AllowRuntimePanics(allow bool)

AllowRuntimePanics controls whether the library may panic at runtime. When true, operations that would normally log warnings or fail silently will instead panic immediately (e.g., modifying static errors). When false (default), the library avoids panics and uses error logging or silent failures where appropriate.

Note: The ID() method will always panic regardless of this setting.

Enabling this is recommended during development and testing to catch programming errors early, but should typically be disabled in production.

func AllowStaticMutations

func AllowStaticMutations(allow bool, shouldPanic bool)

AllowStaticMutations controls whether static errors in the global registry can be mutated. When set to false (default), builder methods on static errors will silently return the original error without modifications. When set to true, shoudlPanic is ignored and mutations are allowed but warnings are logged if internal logging is enabled.

If shouldPanic is true and allow is false, attempts to modify static errors will panic instead of failing silently. This overrides the default silent behavior.

This is a convenience wrapper for global.AllowStaticMutations(allow, shouldPanic).

func BackoffConstant

func BackoffConstant(d time.Duration) func(int) time.Duration

func BackoffExponential

func BackoffExponential(base time.Duration) func(int) time.Duration

func BackoffLinear

func BackoffLinear(step time.Duration) func(int) time.Duration

func ExportIDList

func ExportIDList() ([]byte, error)

ExportIDList returns all registered error IDs as JSON bytes

func GetDebug

func GetDebug(err error) ([]string, bool)

GetDebug extracts debug information from an error

func GetInternalMessage

func GetInternalMessage(err error) string

GetInternalMessage extracts the internal message from an error

func GetMessage

func GetMessage(err error) string

GetMessage extracts the user-facing message from any error

func GetMeta

func GetMeta(err error, key string) (any, bool)

GetMeta extracts metadata from an error

func GetTraces

func GetTraces(err error) ([]string, bool)

GetTraces extracts trace information from an error

func Is

func Is(err error, id ErrorID) bool

Is checks if the target error is an Error with the specified ID

func IsDomain

func IsDomain(err error) bool

IsDomain checks if an error is a domain error

func IsRegistered

func IsRegistered(err error) bool

IsRegistered checks if a generic error is an Error and was registered to a registry

func IsRetryableDefault

func IsRetryableDefault(err error) bool

func IsStatic

func IsStatic(err error) bool

IsStatic checks if an error is a static error

func IsSystem

func IsSystem(err error) bool

IsSystem checks if an error is a system error

func Must

func Must(err error)

Must panics if the error is not nil Useful for initialization code

func MustRegisterTranslator

func MustRegisterTranslator(t Translator)

func On

func On(t HookType, fn any)

On is a global convenience for setting hooks

func OnCreate

func OnCreate(fn func(*Error, map[string]any))

func OnForm

func OnForm(fn func(ErrorID, *Error))

func OnFromFail

func OnFromFail(fn func(error))

func OnFromSuccess

func OnFromSuccess(fn func(error, *Error))

func OnLog

func OnLog(fn func(*Error, map[string]any))

func OnMap

func OnMap(fn func(*Error, map[string]any))

func OnTrace

func OnTrace(fn func(*Error, map[string]any))

func OnTranslate

func OnTranslate(fn func(*Error, map[string]any))

func OnWrap

func OnWrap(fn func(*Error, error))

func OverrideAllowIDRuntimePanics

func OverrideAllowIDRuntimePanics(allow bool)

OverrideAllowIDRuntimePanics sets global id registry override

func OverrideAllowIDRuntimeRegistrationForTestingOnly

func OverrideAllowIDRuntimeRegistrationForTestingOnly(allow bool)

OverrideAllowIDRuntimeRegistrationForTestingOnly sets global id registry override for tests

func Register

func Register(def ErrorDefinition)

Register adds an error definition to the global registry

func RegisterLocalizations added in v3.1.0

func RegisterLocalizations(locale string, msgs map[ErrorID]string)

RegisterLocalizations adds translations for a locale on the global registry

func RegisterMapper

func RegisterMapper(mapper Mapper)

RegisterMapper adds a generic error mapper

func RegisterTranslator

func RegisterTranslator(t Translator) error

RegisterTranslator adds a translator for converting errors to other formats

func Retry

func Retry(fn func() error) error

Retry executes a function with retries

func RetryCFG

func RetryCFG(config RetryConfig, fn func() error) error

RetryCFG executes a function with retries using the passed config

func RetryValue

func RetryValue[T any](fn func() (T, error)) (T, error)

RetryValue retries a function that returns (T, error)

func RetryValueCFG

func RetryValueCFG[T any](config RetryConfig, fn func() (T, error)) (T, error)

func SetDefaultLocale

func SetDefaultLocale(locale string)

SetDefaultLocale sets the fallback locale for the global registry

func SetLocalizer added in v3.1.0

func SetLocalizer(l Localizer)

SetLocalizer sets the global localization provider

func SetLogger

func SetLogger(logger Logger)

SetLogger sets the custom logging solution to the registry

func SetRetryConfig

func SetRetryConfig(config *RetryConfig)

func SetTracer

func SetTracer(tracer Tracer)

SetTracer sets the custom tracing solution to the global registry

func To

func To(err *Error, translatorName string) (any, error)

To converts a fail.Error to an external format using the named translator Only registered errors can be translated (safety guarantee)

func ToAs

func ToAs[T any](err *Error, translatorName string) (T, error)

ToAs is the generic version for global registry

func ToAsFrom

func ToAsFrom[T any](r *Registry, err *Error, translatorName string) (T, error)

ToAsFrom is the generic version for a specific registry

func ValidateIDs

func ValidateIDs()

ValidateIDs checks for gaps and duplicates. Call in main() after all init().

func WithJitter

func WithJitter(delay func(int) time.Duration, jitterFraction float64) func(int) time.Duration

Types

type Error

type Error struct {
	// Required fields
	// FIXME ID should be private to not let users perform 'surgery on errors'
	ID              ErrorID // Unique trusted identifier
	Message         string  // User-facing message
	InternalMessage string  // Internal/debug message (optional but recommended)
	Cause           error   // The underlying error that caused this
	IsSystem        bool    // true = infrastructure/unexpected, false = domain/expected

	Args   []any  // Captured arguments for localization
	Locale string // Target locale for this error instance

	// Optional structured data
	Meta map[string]any // Arbitrary metadata (traces, validation errors, etc.)
	// contains filtered or unexported fields
}

Error is the core error type that all domain errors implement

func As

func As(err error) (*Error, bool)

As extracts an Error from any error

func AsFail added in v3.2.0

func AsFail(err error) *Error

AsFail ensures the error is a *fail.Error. If err is already a *fail.Error, returns it as-is. If err is a generic error, converts via From() (uses mappers). If err is nil, returns nil.

This is useful at boundaries where you have generic errors but need *fail.Error for translation, hooks, or metadata access.

Example:

resp, _ := fail.To(fail.AsFail(err), "http")

func Fast

func Fast(id ErrorID, message string) *Error

Fast creates a simple error with just an ID and custom message

func Form

func Form(id ErrorID, defaultMsg string, isSystem bool, meta map[string]any, defaultArgs ...any) *Error

Form creates, registers, and returns an error in one call This is a convenience function for defining error sentinels

WARNING Only use package level sentinel errors that are created by Form in non-concurrent environments For concurrent environment prefer calling New with the error ID

Example:

var ErrUserNotFound = fail.Form(UserNotFound, "user not found", false, nil)

This is equivalent to:

fail.Register(fail.ErrorDefinition{
    ID:             UserNotFound,
    DefaultMessage: "user not found",
    IsSystem:       false,
})
var ErrUserNotFound = fail.New(UserNotFound)

func From

func From(err error) *Error

From ingests a generic error and maps it to an Error

func FromWithMsg

func FromWithMsg(err error, message string) *Error

FromWithMsg ingests a generic error and adds a custom message

func MustNew

func MustNew(id ErrorID) *Error

MustNew creates an error and panics if it's not registered

func New

func New(id ErrorID) *Error

New returns a new Error from a registered definition

func Newf

func Newf(id ErrorID, format string, args ...interface{}) *Error

Newf returns a new Error from a registered definition with a new formatted message

func Record added in v3.2.0

func Record(e *Error) *Error

Record automatically traces the error using the configured tracer

func RecordCtx added in v3.2.0

func RecordCtx(ctx context.Context, e *Error) *Error

func RegisterMany

func RegisterMany(defs ...*ErrorDefinition) *Error

RegisterMany registers multiple error definitions at once

func Wrap

func Wrap(id ErrorID, cause error) *Error

Wrap creates an error that wraps another error

func WrapMsg

func WrapMsg(id ErrorID, message string, cause error) *Error

WrapMsg creates an error with a custom message that wraps another error

func (*Error) AddLocalization

func (e *Error) AddLocalization(locale string, msg string) *Error

AddLocalization adds a translation for this error's ID to its registry If localization already exists for this locale+ID, does nothing (idempotent) Returns the original error unmodified for chaining

func (*Error) AddLocalizations

func (e *Error) AddLocalizations(msgs map[string]string) *Error

AddLocalizations adds multiple translations at once

func (*Error) AddMeta

func (e *Error) AddMeta(key string, value any) *Error

AddMeta sets a metadata value

func (*Error) Clone added in v3.2.0

func (e *Error) Clone() *Error

Clone lets you safely clone a fail.Error

func (*Error) Debug

func (e *Error) Debug(debug string) *Error

Debug adds debug information to metadata

func (*Error) Debugs

func (e *Error) Debugs(debug ...string) *Error

Debugs adds each debug information to metadata

func (*Error) Domain

func (e *Error) Domain() *Error

Domain marks this error as a domain error

func (*Error) Dump

func (e *Error) Dump() map[string]any

func (*Error) Error

func (e *Error) Error() string

Error() uses GetRendered() for the final message

func (*Error) FromGlobalRegistry

func (e *Error) FromGlobalRegistry() bool

FromGlobalRegistry checks if the Error is from the global Registry and return true or false accordingly

func (*Error) FromRegistry

func (e *Error) FromRegistry(r *Registry) bool

FromRegistry checks if the Error is from the passed in Registry and return true or false accordingly

func (*Error) GetLocalized

func (e *Error) GetLocalized() string

GetLocalized returns the localized message template as string (read-only, no modification)

func (*Error) GetRegistry

func (e *Error) GetRegistry() *Registry

func (*Error) GetRendered

func (e *Error) GetRendered() string

GetRendered returns the fully rendered message as string (read-only, no modification)

func (*Error) Internal

func (e *Error) Internal(msg string) *Error

Internal sets or overrides the internal message

func (*Error) Internalf

func (e *Error) Internalf(format string, args ...any) *Error

Internalf sets the internal message using format string

func (*Error) IsRegistered

func (e *Error) IsRegistered() bool

IsRegistered checks if an error was registered to a registry

func (*Error) Localize

func (e *Error) Localize() *Error

Localize resolves the translated message template for this error's locale Stores result in e.Message, returns *Error for chaining

func (*Error) Log

func (e *Error) Log() *Error

Log automatically logs the error using the configured logger

func (*Error) LogAndRecord

func (e *Error) LogAndRecord() *Error

LogAndRecord logs and traces the error

func (*Error) LogAndRecordCtx

func (e *Error) LogAndRecordCtx(ctx context.Context) *Error

func (*Error) LogCtx

func (e *Error) LogCtx(ctx context.Context) *Error

func (*Error) MergeMeta

func (e *Error) MergeMeta(data map[string]any) *Error

MergeMeta merges a map into the metadata

func (*Error) Msg

func (e *Error) Msg(msg string) *Error

Msg sets or overrides the error message (for Dynamic errors)

func (*Error) Msgf

func (e *Error) Msgf(format string, args ...any) *Error

Msgf sets the error message using format string

func (*Error) Record

func (e *Error) Record() *Error

Record automatically traces the error using the configured tracer

func (*Error) RecordCtx

func (e *Error) RecordCtx(ctx context.Context) *Error

func (*Error) Render

func (e *Error) Render() *Error

Render formats the error's message template with its arguments Stores result in e.Message, returns *Error for chaining

func (*Error) System

func (e *Error) System() *Error

System marks this error as a system error

func (*Error) Trace

func (e *Error) Trace(trace string) *Error

Trace adds trace information to metadata

func (*Error) Traces

func (e *Error) Traces(trace ...string) *Error

Traces adds each trace information to metadata

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap implements error unwrapping for errors.Is/As

func (*Error) Validation

func (e *Error) Validation(field, message string) *Error

Validation adds a validation error to metadata

func (*Error) Validations

func (e *Error) Validations(errs []ValidationError) *Error

Validations adds multiple validation errors at once

func (*Error) With

func (e *Error) With(cause error) *Error

With sets the cause of the error

func (*Error) WithArgs

func (e *Error) WithArgs(args ...any) *Error

WithArgs sets the arguments for template formatting

func (*Error) WithLocale

func (e *Error) WithLocale(locale string) *Error

WithLocale sets the target locale for this error

func (*Error) WithMeta

func (e *Error) WithMeta(data map[string]any) *Error

WithMeta sets the metadata to data, it replaces existing metadata to merge use MergeMeta

type ErrorChain

type ErrorChain struct {
	// contains filtered or unexported fields
}

ErrorChain enables fluent error handling with *Error as first-class citizen internally Accepts standard Go error functions externally, converts immediately to *Error

func Chain

func Chain(fn func() error) *ErrorChain

Chain starts a new error chain, immediately executing the first step The error is normalized to *Error via From() on entry

Example:

err := fail.Chain(validateRequest).
	Then(checkPermissions).
	ThenCtx("database", saveData).
	Error()

func ChainCtx

func ChainCtx(stepName string, fn func() error) *ErrorChain

ChainCtx starts a chain with immediate named context

func (*ErrorChain) Catch

func (c *ErrorChain) Catch(fn func(*Error) *Error) *ErrorChain

Catch transforms the error, enabling recovery or enrichment fn receives the current *Error and returns modified *Error

func (*ErrorChain) Error

func (c *ErrorChain) Error() *Error

Error returns the final *Error or nil Returns concrete type for rich error handling (ID, Meta, Cause, etc.)

func (*ErrorChain) Finally

func (c *ErrorChain) Finally(fn func()) *ErrorChain

Finally executes cleanup regardless of error state

func (*ErrorChain) OnError

func (c *ErrorChain) OnError(fn func(*Error)) *ErrorChain

OnError executes callback if chain has failed (for logging/metrics/cleanup)

func (*ErrorChain) Step

func (c *ErrorChain) Step() int

Step returns count of successfully completed steps

func (*ErrorChain) Then

func (c *ErrorChain) Then(fn func() error) *ErrorChain

Then executes the next step if no error has occurred Automatically converts returned error to *Error via From()

func (*ErrorChain) ThenCtx

func (c *ErrorChain) ThenCtx(stepName string, fn func() error) *ErrorChain

ThenCtx executes with named step context for observability

func (*ErrorChain) ThenCtxIf

func (c *ErrorChain) ThenCtxIf(condition bool, stepName string, fn func() error) *ErrorChain

ThenCtxIf executes named step conditionally

func (*ErrorChain) ThenIf

func (c *ErrorChain) ThenIf(condition bool, fn func() error) *ErrorChain

ThenIf executes conditionally only if condition is true and no prior error

func (*ErrorChain) Unwrap

func (c *ErrorChain) Unwrap() error

Unwrap implements error interface for compatibility

func (*ErrorChain) Valid

func (c *ErrorChain) Valid() bool

Valid returns true if no errors occurred (all steps succeeded)

type ErrorDefinition

type ErrorDefinition struct {
	ID             ErrorID
	DefaultMessage string // Used for Static errors or as fallback
	IsSystem       bool
	Meta           map[string]any // Default metadata to include
	DefaultArgs    []any
}

ErrorDefinition is the blueprint for creating errors

type ErrorGroup

type ErrorGroup struct {
	// contains filtered or unexported fields
}

ErrorGroup collects multiple errors thread-safely

func NewErrorGroup

func NewErrorGroup(capacity int) *ErrorGroup

NewErrorGroup creates a new error group

func (*ErrorGroup) Add

func (g *ErrorGroup) Add(err error) *ErrorGroup

Add adds an error to the group (nil-safe, concurrency-safe)

func (*ErrorGroup) Addf

func (g *ErrorGroup) Addf(id ErrorID, format string, args ...interface{}) *ErrorGroup

Addf adds a formatted error string as a dynamic error (convenience method)

func (*ErrorGroup) Any

func (g *ErrorGroup) Any(match func(*Error) bool) bool

func (*ErrorGroup) Collect

func (g *ErrorGroup) Collect(err error)

Collect is the same as Add but without returning the error group

func (*ErrorGroup) Error

func (g *ErrorGroup) Error() string

Error implements error interface

func (*ErrorGroup) Errors

func (g *ErrorGroup) Errors() []*Error

Errors returns a copy of all collected errors (concurrency-safe)

func (*ErrorGroup) First

func (g *ErrorGroup) First() *Error

First returns the first error or nil (concurrency-safe)

func (*ErrorGroup) HasErrors

func (g *ErrorGroup) HasErrors() bool

HasErrors returns true if the group has any errors

func (*ErrorGroup) Last

func (g *ErrorGroup) Last() *Error

Last returns the last error or nil (useful for "most recent error" scenarios)

func (*ErrorGroup) Len

func (g *ErrorGroup) Len() int

Len returns the number of errors collected (concurrency-safe)

func (*ErrorGroup) Reset

func (g *ErrorGroup) Reset() *ErrorGroup

Reset clears all errors (useful for pooling/reuse scenarios)

func (*ErrorGroup) ToError

func (g *ErrorGroup) ToError() *Error

ToError converts the group to a single *Error Returns nil if no errors, the first error if only one, or a FailMultipleErrors error containing all errors in meta if multiple

func (*ErrorGroup) Unwrap

func (g *ErrorGroup) Unwrap() []error

Unwrap implements the Go 1.20+ multiple error unwrapping interface This allows errors.Is() and errors.As() to check against any error in the group

type ErrorID

type ErrorID struct {
	// contains filtered or unexported fields
}

ErrorID represents a trusted, deterministically-generated error identifier IDs are generated from names using explicit numbering for stability across versions Static (S) and Dynamic (D) have separate counters within each domain Format: LEVEL_DOMAIN_NUM_TYPE (e.g., "0_AUTH_0042_S") Level indicates severity but does not affect uniqueness

func GetID

func GetID(err error) (ErrorID, bool)

GetID extracts the error ID from any error

func ID

func ID(level int, domain string, number int, static bool, name string) ErrorID

ID creates a new trusted ErrorID with explicit numbering This is the ONLY way to create a trusted ErrorID

WARNING: Must be called at package level (var declaration). Calling inside func init() or runtime functions causes unstable numbering. Use go:generate or static analysis to verify.

CRITICAL: Since ErrorIDs are a global concept used throughout your entire codebase, they should ideally all be defined together in the same var block within a single package (e.g., a dedicated errors package). The default global registry cannot ensure uniqueness across packages and may incorrectly report gaps that don't actually exist due to Go's file compilation ordering. If IDs are scattered across multiple files/packages, the registry may see partial sequences and panic on apparent gaps that would be filled by later compilation units. Centralize all ID definitions in one location to ensure strict sequential numbering and avoid false gap detection.

NOTE: The global registry ALWAYS enforces strict sequential numbering (no gaps). If you need non-sequential numbering or want to disable gap checking, you must create your own isolated IDRegistry via NewIDRegistry() and use registry.ID() directly instead of this package-level ID() function. Custom registries allow you to control gap checking behavior, but the global registry is strict by design to ensure consistency across the entire codebase.

Parameters:

  • name: Full error name (e.g., "AuthInvalidCredentials", "UserNotFound")
  • domain: Error domain (e.g., "AUTH", "USER") - must be a prefix of the name
  • static: true for static message, false for dynamic
  • level: severity level (0-9 recommended)
  • number: explicit number for this ID (must be unique within domain+type)

Panics if:

  • Name doesn't start with domain
  • Name already exists in registry
  • Name is too similar to existing name (Levenshtein distance < 3)
  • Domain is "FAIL" (reserved for internal errors)
  • Number already used in this domain+type combination
  • Any gap in numbering is detected that is not being filled by current insertion

Example:

// Centralized in one package - RECOMMENDED
var (
    AuthInvalidCredentials = fail.ID(0, "AUTH", 0, true, "AuthInvalidCredentials")   // 0_AUTH_0000_S
    AuthInvalidPassword    = fail.ID(0, "AUTH", 1, true, "AuthInvalidPassword")      // 0_AUTH_0001_S
    AuthCustomError        = fail.ID(0, "AUTH", 0, false, "AuthCustomError")         // 0_AUTH_0000_D
    AuthAnotherError       = fail.ID(0, "AUTH", 1, false, "AuthAnotherError")        // 0_AUTH_0001_D
    // v0.0.2 - add new ID, must be next in sequence
    AuthNewFeature         = fail.ID(0, "AUTH", 2, true, "AuthNewFeature")           // 0_AUTH_0002_S
)

func (ErrorID) Domain

func (id ErrorID) Domain() string

Domain returns the error domain (e.g., "AUTH", "USER")

func (ErrorID) IsRegistered

func (id ErrorID) IsRegistered() bool

IsRegistered returns true if this ID was created through the proper ID() function

func (ErrorID) IsStatic

func (id ErrorID) IsStatic() bool

IsStatic returns true if this is a static error

func (ErrorID) Level

func (id ErrorID) Level() int

Level returns the severity level

func (ErrorID) Name

func (id ErrorID) Name() string

Name returns the full error name (e.g., "AuthInvalidCredentials")

func (ErrorID) Number

func (id ErrorID) Number() int

Number returns the error number (explicitly assigned for stability)

func (ErrorID) String

func (id ErrorID) String() string

String returns the formatted error ID (e.g., "0_AUTH_0042_S")

type ErrorType

type ErrorType string

ErrorType indicates whether the error message is static or dynamic

const (
	Static  ErrorType = "S" // Message won't change between occurrences
	Dynamic ErrorType = "D" // Message can vary per occurrence
)

type Frame

type Frame struct {
	Function string `json:"function"`
	File     string `json:"file"`
	Line     int    `json:"line"`
	Package  string `json:"package,omitempty"`
}

Frame represents a single stack frame for error traces

func CaptureStack

func CaptureStack(skip int) []Frame

CaptureStack creates a []Frame from runtime

type HookType

type HookType int
const (
	HookCreate HookType = iota
	HookLog
	HookTrace
	HookMap
	HookWrap
	HookFromFail
	HookFromSuccess
	HookForm
	HookTranslate
)

type Hooks

type Hooks struct {
	OnMap []func(*Error, map[string]any)
	// contains filtered or unexported fields
}

Hooks manages lifecycle callbacks for errors Access via Registry.Hooks

func (*Hooks) On

func (h *Hooks) On(t HookType, fn any)

On registers a hook with compile-time friendly type validation (no reflect) Panics immediately if function signature doesn't match HookType

type IDRegistry

type IDRegistry struct {
	// contains filtered or unexported fields
}

IDRegistry manages error ID generation and validation Numbers are explicitly assigned per domain and type (static/dynamic)

func NewIDRegistry

func NewIDRegistry() *IDRegistry

NewIDRegistry creates a new isolated ID registry (useful for testing or multi-app)

func (*IDRegistry) ExportIDList

func (r *IDRegistry) ExportIDList() ([]byte, error)

ExportIDList returns all registered error IDs as JSON for this registry

func (*IDRegistry) GetAllIDs

func (r *IDRegistry) GetAllIDs() []ErrorID

GetAllIDs returns all registered error IDs sorted by domain, type, then number

func (*IDRegistry) ID

func (r *IDRegistry) ID(level int, domain string, number int, static bool, name string) ErrorID

ID creates a new trusted ErrorID for this registry

func (*IDRegistry) OverrideAllowRuntimePanics

func (r *IDRegistry) OverrideAllowRuntimePanics(allow bool)

OverrideAllowRuntimePanics sets per-registry override

func (*IDRegistry) Reset

func (r *IDRegistry) Reset()

Reset clears all registered IDs (useful for testing)

func (*IDRegistry) ValidateIDs

func (r *IDRegistry) ValidateIDs()

ValidateIDs checks for gaps and duplicates. Call in main() after all init().

type Localizer added in v3.1.0

type Localizer interface {
	// Localize returns the localized template for the given error ID and locale
	// Returns empty string if no translation exists
	Localize(id ErrorID, locale string) string

	// RegisterLocalization adds a single translation (for AddLocalization)
	RegisterLocalization(id ErrorID, locale string, template string)

	// RegisterLocalizations adds multiple translations for a locale (for bulk operations)
	RegisterLocalizations(locale string, translations map[ErrorID]string)
}

type Logger

type Logger interface {
	Log(err *Error)
	LogCtx(ctx context.Context, err *Error)
}

Logger allows users to provide their own logging solution

type Mapper

type Mapper interface {
	Name() string
	Priority() int

	// Map : should map generic errors to fail.Error type
	Map(err error) (*Error, bool)
}

Mapper converts errors in any direction: generic->fail, fail->generic, fail->fail, etc.

IMPORTANT: Map must return errors created via fail.New() or fail.From(), not hand-crafted *fail.Error structs. Hand-crafted errors will be unregistered and may cause issues with translators and other components that require registered errors.

type MapperList

type MapperList struct {
	// contains filtered or unexported fields
}

MapperList keeps mappers sorted by priority using container/list

func NewMapperList

func NewMapperList() *MapperList

NewMapperList creates a new MapperList. If includeDefault is true, adds the default mapper with priority -1

func (*MapperList) Add

func (ml *MapperList) Add(m Mapper)

Add inserts a mapper into the list by descending priority

func (*MapperList) Map

func (ml *MapperList) Map(err error) (*Error, string, bool)

Map maps to *fail.Error

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds all registered error definitions and mappers

func MustNewRegistry

func MustNewRegistry(name string) *Registry

MustNewRegistry creates a new isolated registry (for testing or multi-app scenarios) Panics if a registry of the same name is already registered

func NewRegistry

func NewRegistry(name string) (*Registry, error)

NewRegistry creates a new isolated registry (for testing or multi-app scenarios)

func (*Registry) AllowInternalLogs

func (r *Registry) AllowInternalLogs(allow bool)

AllowInternalLogs enables or disables internal library logging for this registry. See AllowInternalLogs for details.

func (*Registry) AllowStaticMutations

func (r *Registry) AllowStaticMutations(allow bool, shouldPanic bool)

AllowStaticMutations controls whether static errors in this registry can be mutated. When allow is false (default), builder methods on static errors return the original error unchanged. When allow is true shoudlPanic is ignored, mutations are permitted but may log warnings if internal logging is enabled.

If shouldPanic is true and allow is false, any builder method called on a static error will panic immediately with a descriptive message. This is useful for catching programming errors during development.

This method is safe for concurrent use.

func (*Registry) Form

func (r *Registry) Form(id ErrorID, defaultMsg string, isSystem bool, meta map[string]any, defaultArgs ...any) *Error

func (*Registry) From

func (r *Registry) From(err error) *Error

From ingests a generic error and maps it to an Error

func (*Registry) New

func (r *Registry) New(id ErrorID) *Error

func (*Registry) On

func (r *Registry) On(t HookType, fn any)

On is a convenience for setting hooks on custom registries

func (*Registry) Register

func (r *Registry) Register(err *Error) *Error

Register adds an error definition to this registry

func (*Registry) RegisterLocalizations added in v3.1.0

func (r *Registry) RegisterLocalizations(locale string, msgs map[ErrorID]string)

RegisterLocalizations adds translations for a locale in a specific registry

func (*Registry) RegisterMany

func (r *Registry) RegisterMany(defs ...*ErrorDefinition) *Error

func (*Registry) RegisterMapper

func (r *Registry) RegisterMapper(mapper Mapper)

func (*Registry) RegisterTranslator

func (r *Registry) RegisterTranslator(t Translator) error

func (*Registry) SetDefaultLocale

func (r *Registry) SetDefaultLocale(locale string)

SetDefaultLocale sets the fallback locale for the specific registry

func (*Registry) SetLocalizer added in v3.1.0

func (r *Registry) SetLocalizer(l Localizer)

SetLocalizer sets the localization provider for this registry

func (*Registry) SetLogger

func (r *Registry) SetLogger(logger Logger)

func (*Registry) SetTracer

func (r *Registry) SetTracer(tracer Tracer)

func (*Registry) To

func (r *Registry) To(err *Error, translatorName string) (zero any, retErr error)

To converts a fail.Error to an external format using the named translator Only registered errors can be translated (safety guarantee)

type RetryConfig

type RetryConfig struct {
	MaxAttempts int
	ShouldRetry func(error) bool

	// Delay returns how long to wait BEFORE the next attempt.
	// attempt starts at 1 for the first retry (not the first call).
	Delay func(attempt int) time.Duration
}

RetryConfig helper for transient errors

type Tracer

type Tracer interface {
	// Record records an error occurrence (simple version)
	Record(err *Error) *Error

	// RecordCtx records an error with context (for spans, baggage, etc.)
	RecordCtx(ctx context.Context, err *Error) *Error
}

Tracer allows users to provide their own tracing solution

type Translator

type Translator interface {
	Name() string

	// Supports reports whether this translator is allowed to translate this error.
	// This is for capability, boundary, or policy checks — not mapping.
	Supports(*Error) error

	// Translate converts the error into an external representation.
	// It may still fail even if Supports returned true (e.g. missing metadata).
	Translate(*Error) (any, error)
}

Translator converts a trusted registry Error into another format (HTTP response, gRPC status, CLI output, etc.)

type UNSET

type UNSET struct{}

type ValidationError

type ValidationError struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

ValidationError represents a field validation error

func GetValidations

func GetValidations(err error) ([]ValidationError, bool)

GetValidations extracts validation errors from an error

func NewValidationError added in v3.2.0

func NewValidationError(field, msg string) ValidationError

NewValidationError is a helper for initializing a validation error

Directories

Path Synopsis
plugins

Jump to

Keyboard shortcuts

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