errors

package module
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 17, 2025 License: MPL-2.0 Imports: 8 Imported by: 13

README

go-errors: Structured, contextual error handling for Go

an AGILira library

go-errors is a fast, structured, and context-aware error handling library for Go. Originally built for Orpheus, it provides error codes, stack traces, user messages, and JSON support with near-zero overhead through Timecache integration.

CI Security Go Report Card Coverage pkg.go.dev

FeaturesPerformanceQuick StartJSON OutputTesting & CoverageDocumentation

Features

  • Structured error type: code, message, context, cause, severity
  • Stacktrace support (optional, lightweight)
  • User and technical messages
  • Custom error codes (user-defined)
  • JSON serialization for API/microservices
  • Retryable and interface-based error handling
  • Helpers for wrapping, root cause, code search
  • Modular, fully tested, high coverage

Compatibility and Support

go-errors is designed for Go 1.23+ environments and follows Long-Term Support guidelines to ensure consistent performance across production deployments.

Performance

AMD Ryzen 5 7520U with Radeon Graphics
BenchmarkNew-8                      12971414       86.61 ns/op     208 B/op     2 allocs/op
BenchmarkNewWithField-8             12536019       87.98 ns/op     208 B/op     2 allocs/op
BenchmarkNewWithContext-8           20815206       57.17 ns/op     160 B/op     1 allocs/op
BenchmarkWrap-8                      2111182       558.0 ns/op     264 B/op     4 allocs/op
BenchmarkMethodChaining-8            5201632       220.8 ns/op     504 B/op     3 allocs/op
BenchmarkHasCode-8                 325451757        3.66 ns/op       0 B/op     0 allocs/op
BenchmarkRootCause-8               144666518        8.18 ns/op       0 B/op     0 allocs/op
BenchmarkMarshalJSON-8                449632        2603 ns/op     568 B/op     7 allocs/op

Reproduce benchmarks:

go test -bench=. -benchmem

Quick Start

Installation
go get github.com/agilira/go-errors
Quick Example
import "github.com/agilira/go-errors"

const ErrCodeValidation = "VALIDATION_ERROR"

func validateUser(username string) error {
    if username == "" {
        return errors.New(ErrCodeValidation, "Username is required").WithUserMessage("Please enter a username.")
    }
    return nil
}

JSON Output

Errors automatically serialize to structured JSON:

err := errors.New("VALIDATION_ERROR", "Email format invalid").
    WithUserMessage("Please enter a valid email address").
    WithContext("field", "email").
    WithSeverity("warning")

jsonData, _ := json.Marshal(err)
// Output: {"code":"VALIDATION_ERROR","message":"Email format invalid","user_msg":"Please enter a valid email address","context":{"field":"email"},"severity":"warning","timestamp":"2025-01-27T10:30:00Z",...}

Testing & Coverage

Run all tests:

go test -v ./...

Check coverage:

go test -cover ./...
  • Write tests for all custom error codes and logic in your application.
  • Use table-driven tests for error scenarios.
  • Aim for high coverage to ensure reliability.

Documentation

Comprehensive documentation is available in the docs folder:


go-errors • an AGILira library

Documentation

Overview

Package errors provides structured, contextual error handling for Go applications.

Overview

This package offers a comprehensive error handling system designed for modern Go applications. It provides structured error types with rich metadata, stack traces, user-friendly messages, and JSON serialization capabilities. Built for microservices and API development, it enables consistent error handling across your entire application stack.

Key Features

• Structured Error Types: Errors include codes, messages, context, timestamps, and severity levels • Stack Trace Support: Optional lightweight stack traces for debugging • User-Friendly Messages: Separate technical and user-facing error messages • JSON Serialization: Built-in JSON marshaling for API responses and logging • Retry Logic: Built-in support for retryable errors • Interface-Based: Type-safe error handling through well-defined interfaces • Zero Dependencies: Uses only Go standard library • High Performance: Minimal overhead with efficient memory usage

Quick Start

Create a new error with a custom code:

const ErrCodeValidation = "VALIDATION_ERROR"
err := errors.New(ErrCodeValidation, "Username is required")

Add user-friendly message and context:

err = err.WithUserMessage("Please enter a username").
	WithContext("field", "username").
	WithSeverity("warning")

Wrap existing errors with additional context:

if err := someOperation(); err != nil {
	return errors.Wrap(err, ErrCodeOperation, "Failed to process request")
}

Check error codes programmatically:

if errors.HasCode(err, ErrCodeValidation) {
	// Handle validation error
}

JSON Serialization

Errors automatically serialize to JSON with all metadata:

jsonData, _ := json.Marshal(err)
// Output: {"code":"VALIDATION_ERROR","message":"Username is required",...}

Interface-Based Error Handling

Use interfaces for type-safe error handling:

var coder errors.ErrorCoder = err
code := coder.ErrorCode()

var retry errors.Retryable = err
if retry.IsRetryable() {
	// Implement retry logic
}

var um errors.UserMessager = err
userMsg := um.UserMessage()

Best Practices

1. Define error codes as constants in your application 2. Use WithUserMessage() for errors that will be displayed to users 3. Use WithContext() to add debugging information 4. Use Wrap() to add context to errors from lower-level functions 5. Use AsRetryable() for transient errors that can be retried 6. Use different severity levels for different types of errors

Error Severity Levels

• "error": Standard application errors (default) • "warning": Non-critical issues that should be noted • "info": Informational messages • "critical": Severe errors that require immediate attention

Integration Examples

REST API Handler:

func handleRequest(w http.ResponseWriter, r *http.Request) {
	err := processRequest(r)
	if err != nil {
		apiErr, ok := err.(*errors.Error)
		if ok {
			http.Error(w, apiErr.UserMessage(), getStatusCode(apiErr))
			log.Error("API Error", "error", apiErr.Error(), "code", apiErr.ErrorCode())
		} else {
			http.Error(w, "Internal server error", 500)
		}
	}
}

Database Operations:

func saveUser(user *User) error {
	if err := db.Save(user); err != nil {
		return errors.Wrap(err, ErrCodeDatabase, "Failed to save user").
			WithContext("user_id", user.ID).
			WithUserMessage("Unable to save your information. Please try again.")
	}
	return nil
}

Validation:

func validateUser(user *User) error {
	if user.Email == "" {
		return errors.NewWithField(ErrCodeValidation, "Email is required", "email", user.Email).
			WithUserMessage("Please provide a valid email address")
	}
	return nil
}

Testing

Use table-driven tests for error scenarios:

func TestValidation(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		wantErr bool
		wantCode errors.ErrorCode
	}{
		{"valid email", "[email protected]", false, ""},
		{"empty email", "", true, ErrCodeValidation},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateEmail(tt.input)
			if tt.wantErr {
				assert.True(t, errors.HasCode(err, tt.wantCode))
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

Performance Considerations

• Stack traces are only captured when using Wrap() or explicitly requested • JSON marshaling is optimized for common use cases • Memory usage is minimal with efficient struct layout • No reflection is used in hot paths

Migration from Standard Errors

Replace standard error creation:

// Before
return errors.New("validation failed")

// After
return errors.New(ErrCodeValidation, "validation failed")

Replace error wrapping:

// Before
return fmt.Errorf("operation failed: %w", err)

// After
return errors.Wrap(err, ErrCodeOperation, "operation failed")

Copyright (c) 2025 AGILira - A. Giordano Series: an AGLIra library SPDX-License-Identifier: MPL-2.0

Example (RealWorldUsage)

Example of real-world usage patterns

// Simulate a service layer function
processUser := func(userID string) error {
	// Validate input
	if userID == "" {
		return errors.NewWithField(ErrCodeValidation, "User ID is required", "user_id", userID).
			WithUserMessage("Please provide a valid user ID")
	}

	// Simulate database error
	if userID == "invalid" {
		dbErr := fmt.Errorf("user not found")
		return errors.Wrap(dbErr, ErrCodeDatabase, "Failed to fetch user").
			WithContext("user_id", userID).
			WithUserMessage("User not found")
	}

	return nil
}

// Handle errors
handleError := func(err error) {
	if err == nil {
		return
	}

	structErr, ok := err.(*errors.Error)
	if !ok {
		log.Printf("Unknown error: %v", err)
		return
	}

	// Log technical details
	log.Printf("Error [%s]: %s", structErr.ErrorCode(), structErr.Error())

	// Show user-friendly message
	fmt.Printf("User message: %s\n", structErr.UserMessage())

	// Handle specific error types
	switch {
	case errors.HasCode(err, ErrCodeValidation):
		fmt.Println("Handling validation error")
	case errors.HasCode(err, ErrCodeDatabase):
		fmt.Println("Handling database error")
	}
}

// Test cases
handleError(processUser(""))        // Validation error
handleError(processUser("invalid")) // Database error
handleError(processUser("valid"))   // No error
Output:

User message: Please provide a valid user ID
Handling validation error
User message: User not found
Handling database error

Index

Examples

Constants

View Source
const (
	SeverityCritical = "critical" // System failures, data corruption, security breaches
	SeverityError    = "error"    // Standard errors that prevent operation completion
	SeverityWarning  = "warning"  // Issues that don't prevent operation but need attention
	SeverityInfo     = "info"     // Informational messages for debugging/audit trails
)

Predefined severity levels for consistent error classification. These constants can be used with WithSeverity() method for type safety.

Variables

This section is empty.

Functions

func HasCode

func HasCode(err error, code ErrorCode) bool

HasCode checks if any error in the error chain has the given error code. This is useful for checking if a specific type of error occurred anywhere in the chain.

Example:

if HasCode(err, "VALIDATION_ERROR") {
	// Handle validation-specific error
	log.Warning("Validation failed", "error", err)
}
Example

ExampleHasCode demonstrates error code checking

// Create a chain of wrapped errors
originalErr := fmt.Errorf("disk full")
wrappedErr := errors.Wrap(originalErr, ErrCodeDatabase, "Failed to write data")
doubleWrapped := errors.Wrap(wrappedErr, "OPERATION_FAILED", "User save operation failed")

// Check for specific error codes in the chain
if errors.HasCode(doubleWrapped, ErrCodeDatabase) {
	fmt.Println("Database error detected in chain")
}

if errors.HasCode(doubleWrapped, ErrCodeValidation) {
	fmt.Println("This won't print - no validation error in chain")
} else {
	fmt.Println("No validation error in chain")
}
Output:

Database error detected in chain
No validation error in chain

func RootCause

func RootCause(err error) error

RootCause returns the original error in the error chain by unwrapping all nested errors. This is useful for finding the root cause of an error that has been wrapped multiple times.

Types

type Error

type Error struct {
	Code      ErrorCode              `json:"code"`
	Message   string                 `json:"message"`
	Field     string                 `json:"field,omitempty"`
	Value     string                 `json:"value,omitempty"`
	Context   map[string]interface{} `json:"context,omitempty"`
	Timestamp time.Time              `json:"timestamp"`
	Cause     error                  `json:"cause,omitempty"`
	Severity  string                 `json:"severity"`
	Stack     *Stacktrace            `json:"stack,omitempty"`
	UserMsg   string                 `json:"user_msg,omitempty"`
	Retryable bool                   `json:"retryable,omitempty"`
}

Error represents a structured error with comprehensive context and metadata. It includes error codes, messages, stack traces, user-friendly messages, and retry information.

Example (ChainMethods)

ExampleError_chainMethods demonstrates fluent API usage

err := errors.New(ErrCodeValidation, "User data validation failed").
	WithUserMessage("Please check your information and try again").
	WithContext("user_id", "12345").
	WithContext("validation_errors", []string{"email", "phone"}).
	AsRetryable().
	WithWarningSeverity()

fmt.Printf("Code: %s\n", err.ErrorCode())
fmt.Printf("Retryable: %t\n", err.IsRetryable())
fmt.Printf("Severity: %s\n", err.Severity)
Output:

Code: VALIDATION_ERROR
Retryable: true
Severity: warning
Example (Severity)

ExampleError_severity demonstrates severity levels

// Using predefined severity constants
criticalErr := errors.New(ErrCodeDatabase, "Data corruption detected").
	WithCriticalSeverity()

warningErr := errors.New(ErrCodeValidation, "Optional field missing").
	WithWarningSeverity()

infoErr := errors.New("INFO_CODE", "Operation completed successfully").
	WithInfoSeverity()

fmt.Printf("Critical: %s (severity: %s)\n", criticalErr.Message, criticalErr.Severity)
fmt.Printf("Warning: %s (severity: %s)\n", warningErr.Message, warningErr.Severity)
fmt.Printf("Info: %s (severity: %s)\n", infoErr.Message, infoErr.Severity)
Output:

Critical: Data corruption detected (severity: critical)
Warning: Optional field missing (severity: warning)
Info: Operation completed successfully (severity: info)

func New

func New(code ErrorCode, message string) *Error

New creates a new structured error with the given code and message. The error will have a timestamp set to the current time and default severity of SeverityError. If code is empty or whitespace-only, DefaultErrorCode will be used instead.

Example:

const ErrCodeValidation ErrorCode = "VALIDATION_ERROR"
err := New(ErrCodeValidation, "Username is required")
fmt.Println(err.Error()) // Output: [VALIDATION_ERROR]: Username is required
Example

ExampleNew demonstrates basic error creation

err := errors.New(ErrCodeValidation, "Username is required")
fmt.Println(err.Error())
fmt.Printf("Code: %s\n", err.ErrorCode())
Output:

[VALIDATION_ERROR]: Username is required
Code: VALIDATION_ERROR

func NewWithContext

func NewWithContext(code ErrorCode, message string, context map[string]interface{}) *Error

NewWithContext creates a new structured error with the given code, message, and context map. The context map allows you to attach additional metadata to the error for debugging purposes. If code is empty or whitespace-only, DefaultErrorCode will be used instead.

func NewWithField

func NewWithField(code ErrorCode, message, field, value string) *Error

NewWithField creates a new structured error with the given code, message, field, and value. This is useful for validation errors where you need to specify which field caused the error. If code is empty or whitespace-only, DefaultErrorCode will be used instead.

Example:

err := NewWithField("VALIDATION_ERROR", "Invalid email format", "email", "invalid@")
fmt.Printf("Field: %s, Value: %s\n", err.Field, err.Value)
// Output: Field: email, Value: invalid@
Example

ExampleNewWithField demonstrates field-specific validation errors

err := errors.NewWithField(ErrCodeValidation, "Invalid email format", "email", "invalid@")
fmt.Printf("Error: %s\n", err.Error())
fmt.Printf("Field: %s, Value: %s\n", err.Field, err.Value)
Output:

Error: [VALIDATION_ERROR]: Invalid email format
Field: email, Value: invalid@

func Wrap

func Wrap(err error, code ErrorCode, message string) *Error

Wrap wraps an existing error with a new code and message, capturing the current stack trace. This is useful for adding context to errors that occur deeper in the call stack. If code is empty or whitespace-only, DefaultErrorCode will be used instead.

Example:

if err := someOperation(); err != nil {
	return Wrap(err, "OPERATION_FAILED", "Failed to process user data")
}
Example

ExampleWrap demonstrates error wrapping with stack traces

// Simulate a low-level error
originalErr := fmt.Errorf("connection refused")

// Wrap with structured error
wrappedErr := errors.Wrap(originalErr, ErrCodeNetwork, "Failed to connect to service")

fmt.Printf("Wrapped: %s\n", wrappedErr.Error())
fmt.Printf("Root cause: %s\n", errors.RootCause(wrappedErr).Error())
Output:

Wrapped: [NETWORK_ERROR]: Failed to connect to service
Root cause: connection refused

func (*Error) As

func (e *Error) As(target interface{}) bool

As implements errors.As compatibility for error type assertion. It delegates the check to the underlying Cause error. Note: It will not match the *Error instance itself, only errors in its cause chain.

func (*Error) AsRetryable added in v1.0.2

func (e *Error) AsRetryable() *Error

AsRetryable marks the error as retryable and returns the error for chaining. This indicates that the operation that caused this error can be safely retried.

Example

ExampleError_AsRetryable demonstrates retry logic

retryableErr := errors.New(ErrCodeNetwork, "Temporary service unavailable").
	AsRetryable().
	WithUserMessage("Service temporarily unavailable. Please try again.")

if retryableErr.IsRetryable() {
	fmt.Println("This error can be retried")
	fmt.Printf("User message: %s\n", retryableErr.UserMessage())
}
Output:

This error can be retried
User message: Service temporarily unavailable. Please try again.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface for *Error. It returns a formatted string containing the error code and message.

func (*Error) ErrorCode

func (e *Error) ErrorCode() ErrorCode

ErrorCode returns the error code. This implements the ErrorCoder interface.

func (*Error) Is

func (e *Error) Is(target error) bool

Is implements errors.Is compatibility for error comparison. It returns true if the target error has the same error code.

func (*Error) IsRetryable

func (e *Error) IsRetryable() bool

IsRetryable returns whether the error is retryable. This implements the Retryable interface.

func (*Error) MarshalJSON

func (e *Error) MarshalJSON() ([]byte, error)

MarshalJSON implements custom JSON marshaling for Error. It converts the stack trace to a string representation for JSON serialization.

Example

ExampleError_MarshalJSON demonstrates JSON serialization

err := errors.New(ErrCodeAuth, "Invalid credentials").
	WithUserMessage("Please check your username and password").
	WithContext("attempt", 3).
	WithContext("ip", "192.168.1.1").
	WithWarningSeverity()

jsonData, _ := json.Marshal(err)

// Parse to show structure without exact timestamp
var result map[string]interface{}
_ = json.Unmarshal(jsonData, &result)

fmt.Printf("Code: %s\n", result["code"])
fmt.Printf("Message: %s\n", result["message"])
fmt.Printf("Severity: %s\n", result["severity"])
fmt.Printf("User Message: %s\n", result["user_msg"])
fmt.Printf("Has Context: %t\n", result["context"] != nil)
fmt.Printf("Has Timestamp: %t\n", result["timestamp"] != nil)
Output:

Code: AUTHENTICATION_ERROR
Message: Invalid credentials
Severity: warning
User Message: Please check your username and password
Has Context: true
Has Timestamp: true

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying cause error, implementing the error wrapping interface.

func (*Error) UserMessage

func (e *Error) UserMessage() string

UserMessage returns the user-friendly message if set, otherwise falls back to the technical message. This implements the UserMessager interface.

func (*Error) WithContext added in v1.0.2

func (e *Error) WithContext(key string, value interface{}) *Error

WithContext adds or updates context information on the error and returns the error for chaining. Context information is useful for debugging and logging purposes.

Example

ExampleError_WithContext demonstrates adding debugging context

err := errors.New(ErrCodeDatabase, "Query failed").
	WithContext("query", "SELECT * FROM users").
	WithContext("duration_ms", 5000).
	WithContext("connection_pool", "primary")

fmt.Printf("Error: %s\n", err.Error())
fmt.Printf("Query: %s\n", err.Context["query"])
fmt.Printf("Duration: %v ms\n", err.Context["duration_ms"])
Output:

Error: [DATABASE_ERROR]: Query failed
Query: SELECT * FROM users
Duration: 5000 ms

func (*Error) WithCriticalSeverity added in v1.0.2

func (e *Error) WithCriticalSeverity() *Error

WithCriticalSeverity sets the error severity to critical and returns the error for chaining. Use this for system failures, data corruption, or security breaches.

func (*Error) WithInfoSeverity added in v1.0.2

func (e *Error) WithInfoSeverity() *Error

WithInfoSeverity sets the error severity to info and returns the error for chaining. Use this for informational messages for debugging or audit trails.

func (*Error) WithSeverity added in v1.0.2

func (e *Error) WithSeverity(severity string) *Error

WithSeverity sets the severity level of the error and returns the error for chaining. Common severity levels include "error", "warning", "info", and "critical".

func (*Error) WithUserMessage

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

WithUserMessage sets a user-friendly message on the error and returns the error for chaining. This message should be safe to display to end users without exposing technical details.

Example:

err := New("DB_CONNECTION_ERROR", "Database connection timeout").
	WithUserMessage("We're experiencing technical difficulties. Please try again later.")
Example

ExampleError_WithUserMessage demonstrates user-friendly error messages

err := errors.New(ErrCodeDatabase, "Connection timeout after 30 seconds").
	WithUserMessage("We're experiencing technical difficulties. Please try again later.")

fmt.Printf("Technical: %s\n", err.Message)
fmt.Printf("User-friendly: %s\n", err.UserMessage())
Output:

Technical: Connection timeout after 30 seconds
User-friendly: We're experiencing technical difficulties. Please try again later.

func (*Error) WithWarningSeverity added in v1.0.2

func (e *Error) WithWarningSeverity() *Error

WithWarningSeverity sets the error severity to warning and returns the error for chaining. Use this for issues that don't prevent operation but need attention.

type ErrorCode

type ErrorCode string

ErrorCode represents a custom error code that can be used to categorize and identify specific types of errors. Error codes should be defined as constants in your application for consistency.

const DefaultErrorCode ErrorCode = "UNKNOWN_ERROR"

DefaultErrorCode is used when an empty or invalid ErrorCode is provided to constructors.

type ErrorCoder

type ErrorCoder interface {
	ErrorCode() ErrorCode
}

ErrorCoder allows extracting an error code from an error. This interface enables type-safe error code checking without type assertions.

type Retryable

type Retryable interface {
	IsRetryable() bool
}

Retryable indicates whether an error represents a condition that can be safely retried. This interface is useful for implementing retry logic in applications.

type Stacktrace

type Stacktrace struct {
	Frames []uintptr
}

Stacktrace holds a slice of program counters for error tracing and debugging. It captures the call stack at the time of error creation for detailed debugging information.

func CaptureStacktrace

func CaptureStacktrace(skip int) *Stacktrace

CaptureStacktrace returns a new Stacktrace from the current call stack. The skip parameter determines how many stack frames to skip from the top. Optimized to reduce allocations by using a smaller initial buffer and growing as needed.

func (*Stacktrace) String

func (s *Stacktrace) String() string

String returns a human-readable representation of the stack trace. Each frame is displayed with function name, file path, and line number. Optimized for better performance with pre-allocated buffer size estimation.

type UserMessager

type UserMessager interface {
	UserMessage() string
}

UserMessager allows extracting a user-friendly message from an error. This interface enables displaying safe, non-technical messages to end users.

Jump to

Keyboard shortcuts

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