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 ¶
- Constants
- func HasCode(err error, code ErrorCode) bool
- func RootCause(err error) error
- type Error
- func (e *Error) As(target interface{}) bool
- func (e *Error) AsRetryable() *Error
- func (e *Error) Error() string
- func (e *Error) ErrorCode() ErrorCode
- func (e *Error) Is(target error) bool
- func (e *Error) IsRetryable() bool
- func (e *Error) MarshalJSON() ([]byte, error)
- func (e *Error) Unwrap() error
- func (e *Error) UserMessage() string
- func (e *Error) WithContext(key string, value interface{}) *Error
- func (e *Error) WithCriticalSeverity() *Error
- func (e *Error) WithInfoSeverity() *Error
- func (e *Error) WithSeverity(severity string) *Error
- func (e *Error) WithUserMessage(msg string) *Error
- func (e *Error) WithWarningSeverity() *Error
- type ErrorCode
- type ErrorCoder
- type Retryable
- type Stacktrace
- type UserMessager
Examples ¶
Constants ¶
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 ¶
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
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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 ¶
Error implements the error interface for *Error. It returns a formatted string containing the error code and message.
func (*Error) ErrorCode ¶
ErrorCode returns the error code. This implements the ErrorCoder interface.
func (*Error) Is ¶
Is implements errors.Is compatibility for error comparison. It returns true if the target error has the same error code.
func (*Error) IsRetryable ¶
IsRetryable returns whether the error is retryable. This implements the Retryable interface.
func (*Error) MarshalJSON ¶
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 ¶
Unwrap returns the underlying cause error, implementing the error wrapping interface.
func (*Error) UserMessage ¶
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
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
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
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
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 ¶
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
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.