errx

package module
v0.0.0-...-591fb0a Latest Latest
Warning

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

Go to latest
Published: Jan 9, 2026 License: MIT Imports: 3 Imported by: 0

README

errx

Rich error handling with classification tags, displayable messages, and structured attributes for Go

CI Go Reference Go Report Card Go Version

Overview

errx is a powerful error handling library for Go that extends the standard library's error handling with four key capabilities:

  • Classification Tags: Categorize errors for programmatic checking without cluttering error messages
  • Displayable Errors: Create user-safe messages that can be extracted from error chains
  • Structured Attributes: Attach key-value metadata for logging and debugging
  • Stack Traces (optional): Capture call stacks for debugging via the stacktrace subpackage

The library is designed for developers building production systems that need sophisticated error handling, clear separation between internal and user-facing errors, and rich contextual information for debugging.

Target audience: Backend developers, API engineers, and application architects building robust systems with comprehensive error handling requirements.

Features

  • Hierarchical error classification with sentinel-based error checking
  • User-safe displayable messages separate from internal error details
  • Structured attributes for rich logging and debugging context
  • Optional stack traces via the stacktrace subpackage
  • Zero dependencies in core package (stacktrace uses only Go stdlib)
  • Well-tested with comprehensive test coverage
  • Simple API designed for ease of use and composability
  • Compatible with standard errors.Is() and errors.As()

Requirements

  • Go 1.25+ (tested on 1.25.x)

Installation

Add the library to your Go module:

go get github.com/go-extras/errx@latest

Quick Start

package main

import (
    "errors"
    "fmt"
    "github.com/go-extras/errx"
)

// Define classification sentinels
var (
    ErrNotFound = errx.NewSentinel("resource not found")
    ErrInvalid  = errx.NewSentinel("invalid input")
)

func processOrder(orderID string) error {
    // Create a displayable error
    displayErr := errx.NewDisplayable("Order not found")
    
    // Wrap with context and classification
    return errx.Wrap("failed to process order", displayErr, ErrNotFound)
}

func main() {
    err := processOrder("12345")
    
    // Check error classification
    if errors.Is(err, ErrNotFound) {
        fmt.Println("Resource was not found")
    }
    
    // Extract displayable message
    if errx.IsDisplayable(err) {
        msg := errx.DisplayText(err)
        fmt.Println("User-safe message:", msg)
    }
    
    // Full error for logging
    fmt.Println("Full error:", err)
}

Core Concepts

Classification Sentinels

Classification sentinels let you identify error types without adding text to the error message chain.

// Define sentinels
var (
    ErrDatabase   = errx.NewSentinel("database error")
    ErrNetwork    = errx.NewSentinel("network error")
    ErrValidation = errx.NewSentinel("validation error")
)

// Use Classify to classify an error without adding context
func fetchData() error {
    err := db.Execute("SELECT * FROM data")
    if err != nil {
        return errx.Classify(err, ErrDatabase)
    }
    return nil
}

// Use Wrap to add both context and classification
func getData(id string) error {
    err := fetchData()
    if err != nil {
        return errx.Wrap("failed to get data", err, ErrDatabase)
    }
    return nil
}

// Check the classification
err := getData("123")
if errors.Is(err, ErrDatabase) {
    // Handle database error
}

When to use Classify vs Wrap:

  • Use Classify when the error message is already clear and you just need to classify it
  • Use Wrap when you need to add contextual information about where/why the error occurred
Hierarchical Sentinels

Create hierarchical error taxonomies by passing parent sentinels to NewSentinel:

var (
    // Parent categories
    ErrRetryable    = errx.NewSentinel("retryable")
    ErrPermanent    = errx.NewSentinel("permanent")

    // Child sentinels with parents
    ErrTimeout      = errx.NewSentinel("timeout", ErrRetryable)
    ErrRateLimit    = errx.NewSentinel("rate limit", ErrRetryable)
    ErrNotFound     = errx.NewSentinel("not found", ErrPermanent)
    ErrForbidden    = errx.NewSentinel("forbidden", ErrPermanent)
)

func handleRequest() error {
    err := makeAPICall()
    
    // Check for specific error
    if errors.Is(err, ErrTimeout) {
        // Retry with backoff
        return retryWithBackoff()
    }
    
    // Check for general category
    if errors.Is(err, ErrRetryable) {
        // Any retryable error
        return retry()
    }
    
    // Check for permanent errors
    if errors.Is(err, ErrPermanent) {
        // Don't retry
        return err
    }
    
    return err
}

Multiple Parent Sentinels:

You can also create sentinels with multiple parents for multi-dimensional classification:

var (
    // Classification dimensions
    ErrRetryable = errx.NewSentinel("retryable")
    ErrDatabase  = errx.NewSentinel("database")
    ErrNetwork   = errx.NewSentinel("network")

    // Sentinels with multiple parents
    ErrDatabaseTimeout = errx.NewSentinel("database timeout", ErrDatabase, ErrRetryable)
    ErrNetworkTimeout  = errx.NewSentinel("network timeout", ErrNetwork, ErrRetryable)
)

// Now you can check errors along multiple dimensions
err := query()
if errors.Is(err, ErrDatabase) {
    // Handle any database error
}
if errors.Is(err, ErrRetryable) {
    // Handle any retryable error (database or network)
}
Displayable Messages

Separate user-safe messages from internal error details:

func validateEmail(email string) error {
    if !strings.Contains(email, "@") {
        return errx.NewDisplayable("Please enter a valid email address")
    }
    return nil
}

func createAccount(email string) error {
    err := validateEmail(email)
    if err != nil {
        // Add internal context
        return errx.Wrap("account creation failed", err, ErrValidation)
    }
    return nil
}

// In your API handler
func handleCreateAccount(w http.ResponseWriter, r *http.Request) {
    err := createAccount(email)
    if err != nil {
        // Extract user-safe message
        userMsg := "An error occurred"
        if errx.IsDisplayable(err) {
            userMsg = errx.DisplayText(err)
        }
        
        // Log full error internally
        log.Error("account creation failed", "error", err)
        
        // Send safe message to user
        http.Error(w, userMsg, http.StatusBadRequest)
    }
}
Structured Attributes

Attach key-value metadata for structured logging:

func processPayment(userID string, amount float64) error {
    if amount < 0 {
        // Create attributed error
        attrErr := errx.WithAttrs(
            "user_id", userID,
            "amount", amount,
            "currency", "USD",
        )
        return errx.Wrap("payment validation failed", attrErr, ErrValidation)
    }
    return nil
}

// Extract attributes for logging
err := processPayment("user123", -50.0)
if errx.HasAttrs(err) {
    attrs := errx.ExtractAttrs(err)
    log.Error("payment failed", "error", err, "attributes", attrs)
}
Stack Traces (Optional)

The stacktrace subpackage provides optional stack trace support while keeping the core errx package minimal and zero-dependency:

import (
    "github.com/go-extras/errx"
    "github.com/go-extras/errx/stacktrace"
)

// Option 1: Per-error opt-in using Here()
err := errx.Wrap("operation failed", cause, ErrNotFound, stacktrace.Here())

// Option 2: Automatic capture with stacktrace.Wrap()
err := stacktrace.Wrap("operation failed", cause, ErrNotFound)

// Extract and use stack traces
frames := stacktrace.Extract(err)
if frames != nil {
    for _, frame := range frames {
        fmt.Printf("%s:%d %s\n", frame.File, frame.Line, frame.Function)
    }
}

Key features:

  • Opt-in: Stack traces are only captured when explicitly requested
  • Zero overhead: Core errx package remains dependency-free and fast
  • Composable: Works seamlessly with all other errx features (sentinels, displayable, attributes)
  • Two usage patterns: Per-error with Here() or automatic with stacktrace.Wrap()

See the stacktrace package documentation for more details.

Complete Example

package main

import (
    "errors"
    "fmt"
    "github.com/go-extras/errx"
)

// Define error taxonomy
var (
    // Top-level categories
    ErrClient = errx.NewSentinel("client error")
    ErrServer = errx.NewSentinel("server error")

    // Specific errors
    ErrNotFound     = errx.NewSentinel("not found", ErrClient)
    ErrUnauthorized = errx.NewSentinel("unauthorized", ErrClient)
    ErrDatabase     = errx.NewSentinel("database", ErrServer)
)

// Data layer - adds attributes
func findUserInDB(userID string) error {
    // Simulate database error
    dbErr := errors.New("connection timeout")
    attrErr := errx.WithAttrs("user_id", userID, "operation", "select")
    return errx.Classify(dbErr, attrErr)
}

// Service layer - adds classification and context
func getUser(userID string) error {
    err := findUserInDB(userID)
    if err != nil {
        return errx.Wrap("failed to find user", err, ErrDatabase)
    }
    return nil
}

// API layer - adds displayable message
func handleGetUser(userID string) error {
    err := getUser(userID)
    if err != nil {
        displayErr := errx.NewDisplayable("User not found")
        return errx.Classify(err, displayErr, ErrNotFound)
    }
    return nil
}

func main() {
    err := handleGetUser("user123")
    
    if err != nil {
        // Determine status code from classification
        statusCode := 500
        switch {
        case errors.Is(err, ErrNotFound):
            statusCode = 404
        case errors.Is(err, ErrUnauthorized):
            statusCode = 401
        case errors.Is(err, ErrClient):
            statusCode = 400
        }
        
        // Get user-safe message
        userMsg := "An internal error occurred"
        if errx.IsDisplayable(err) {
            userMsg = errx.DisplayText(err)
        }
        
        // Extract attributes for logging
        if errx.HasAttrs(err) {
            attrs := errx.ExtractAttrs(err)
            fmt.Printf("Error: %v, Status: %d, Attrs: %v\n", 
                err, statusCode, attrs)
        }
        
        // Send response
        fmt.Printf("HTTP %d: %s\n", statusCode, userMsg)
    }
}

Best Practices

1. Define Sentinels at Package Level
package orders

var (
    ErrOrderNotFound = errx.NewSentinel("order not found")
    ErrOrderExpired  = errx.NewSentinel("order expired")
)
2. Add Displayable Messages at Domain Boundaries

Create displayable errors where you validate input or detect user-relevant conditions:

func validateOrder(order Order) error {
    if order.Total < 0 {
        return errx.NewDisplayable("Order total cannot be negative")
    }
    return nil
}
3. Use Classify to Preserve Clear Messages
// The validation error already has a clear message
err := validateOrder(order)
if err != nil {
    // Just add classification, don't wrap
    return errx.Classify(err, ErrInvalid)
}
4. Use Wrap to Add Context
func processOrder(order Order) error {
    err := saveOrder(order)
    if err != nil {
        // Add context about what we were doing
        return errx.Wrap("failed to process order", err, ErrDatabase)
    }
    return nil
}
5. Check Sentinels from Specific to General
switch {
case errors.Is(err, ErrDatabaseTimeout):
    // Handle specific timeout
case errors.Is(err, ErrDatabase):
    // Handle any database error
case errors.Is(err, ErrServer):
    // Handle any server error
}
6. Use Attributes for Structured Logging
// Attach attributes at the point where context is available
if err != nil {
    return attrs.Tag(err, 
        "request_id", reqID,
        "user_id", userID,
        "operation", "create_order",
    )
}
7. Separate Internal and External Errors
func apiHandler(w http.ResponseWriter, r *http.Request) {
    err := businessLogic()
    if err != nil {
        // Always log full internal error
        if errx.HasAttrs(err) {
            attrs := errx.ExtractAttrs(err)
            log.Error("operation failed", "error", err, "attrs", attrs)
        } else {
            log.Error("operation failed", "error", err)
        }
        
        // Only send displayable messages to users
        userMsg := "An error occurred"
        if errx.IsDisplayable(err) {
            userMsg = errx.DisplayText(err)
        }
        http.Error(w, userMsg, determineStatusCode(err))
    }
}

Pattern: Combined Classification and Display

The most powerful pattern combines all three features:

func authenticateUser(username, password string) error {
    user, err := findUser(username)
    if err != nil {
        // Create displayable error
        displayErr := errx.NewDisplayable("Invalid username or password")
        // Add internal context
        wrappedErr := errx.Wrap("authentication failed", displayErr, ErrUnauthorized)
        // Add debugging attributes
        attrErr := errx.WithAttrs("username", username, "reason", "user_not_found")
        return errx.Classify(wrappedErr, attrErr)
    }
    
    if !user.CheckPassword(password) {
        displayErr := errx.NewDisplayable("Invalid username or password")
        wrappedErr := errx.Wrap("password check failed", displayErr, ErrUnauthorized)
        attrErr := errx.WithAttrs("username", username, "reason", "wrong_password")
        return errx.Classify(wrappedErr, attrErr)
    }
    
    return nil
}

// Usage
err := authenticateUser("alice", "wrong")

// Check classification
errors.Is(err, ErrUnauthorized) // true

// Get displayable message
errx.DisplayText(err) // "Invalid username or password"

// Get full error
err.Error() // "authentication failed: password check failed: Invalid username or password"

// Get attributes
attrs := errx.ExtractAttrs(err)
// Convert to map if needed
attrMap := make(map[string]any)
for _, a := range attrs {
    attrMap[a.Key] = a.Value
} // map[username:alice reason:wrong_password]

API Documentation

Full API documentation is available at pkg.go.dev/github.com/go-extras/errx.

Core Functions
Error Creation
  • NewSentinel(message string, parents ...error) error Creates a new sentinel error for classification. Supports hierarchical error taxonomies.

  • NewDisplayable(message string) error Creates a user-safe displayable error message.

  • WithAttrs(keyvals ...any) error Creates an error with structured key-value attributes.

Error Wrapping
  • Wrap(message string, err error, sentinels ...error) error Wraps an error with context and optional classification sentinels.

  • Classify(err error, sentinels ...error) error Adds classification to an error without adding context to the message.

Error Inspection
  • IsDisplayable(err error) bool Checks if an error chain contains a displayable message.

  • DisplayText(err error) string Extracts the displayable message from an error chain.

  • HasAttrs(err error) bool Checks if an error chain contains structured attributes.

  • ExtractAttrs(err error) []Attr Extracts all attributes from an error chain.

Use Cases

  • API error handling: Separate internal errors from user-facing messages
  • Microservices: Classify errors for proper HTTP status code mapping
  • Structured logging: Attach rich context to errors for debugging
  • Error monitoring: Track error categories and patterns
  • Domain-driven design: Create error taxonomies that match your domain

Comparison with Standard Errors

// Standard approach
err := errors.New("user not found")
wrapped := fmt.Errorf("failed to get user: %w", err)

// errx approach
displayErr := errx.NewDisplayable("User not found")
classifiedErr := errx.Wrap("failed to get user", displayErr, ErrNotFound)

// Benefits:
// - Programmatic checking: errors.Is(err, ErrNotFound)
// - Clean user messages: errx.DisplayText(err)
// - Full internal context: err.Error()
// - Structured metadata: errx.ExtractAttrs(err)

Testing

Run the test suite:

# Run all tests
go test ./...

# Run tests with race detection
go test -race ./...

# Run tests with coverage
go test -cover ./...

Contributing

Contributions are welcome! Please:

  • Open issues for bugs, feature requests, or questions
  • Submit pull requests with clear descriptions and tests
  • Follow the existing code style and conventions
  • Ensure all tests pass and maintain test coverage

License

MIT © 2026 Denis Voytyuk — see LICENSE for details.

Acknowledgments

This library builds upon Go's standard errors package and is inspired by best practices from the Go community for error handling in production systems.

Documentation

Overview

Package errx provides error handling utilities with classification sentinels and displayable messages. It enables wrapping errors with classification sentinels that can be checked using errors.Is.

Core Concepts

The package provides three main error categories:

Classification Sentinels: For programmatic error checking using errors.Is. These sentinels are used to identify specific error conditions in code, such as "not found" or "access denied". The sentinel text is intentionally NOT visible in the error message chain to keep error messages clean.

Displayable Errors: For user-facing error messages. These errors represent messages that are safe and appropriate to display directly to end users. They can be extracted from any error chain using DisplayText, which returns just the displayable message without internal context.

When to Use

Use Classification sentinels (NewSentinel) when:

  • You need to check for specific error conditions programmatically
  • The error type is more important than the error message
  • You want to attach error classifications without polluting error messages

Use Displayable errors (NewDisplayable) when:

  • You need to return user-friendly error messages from APIs
  • The error should be safe to display to end users
  • You want to separate internal error context from user messages

Use Wrap when:

  • You need to add context to an error
  • You want to attach classification sentinels to existing errors
  • You're propagating errors up the call stack

Use Classify when:

  • You want to attach classification sentinels WITHOUT adding context text
  • You need to mark an error for programmatic checking but keep the original message
  • You're at a layer where the error message is already sufficient

Example Usage

// Define classification sentinels
var ErrNotFound = errx.NewSentinel("resource not found")

// Create displayable error
func validateInput(email string) error {
    if !isValid(email) {
        return errx.NewDisplayable("Invalid email format")
    }
    return nil
}

// Wrap with context and sentinels
func fetchUser(id string) error {
    displayErr := errx.NewDisplayable("User not found")
    return errx.Wrap("failed to fetch user", displayErr, ErrNotFound)
}

// Classify without adding context
func processRecord(err error) error {
    return errx.Classify(err, ErrNotFound)  // Preserves original message
}

// Check for specific errors
if errors.Is(err, ErrNotFound) {
    // Handle not found case
}

// Extract displayable message
if errx.IsDisplayable(err) {
    return errx.DisplayText(err)  // Returns: "User not found"
}
Example

Example demonstrates basic usage of classification tags

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	// Define classification tags
	ErrNotFound := errx.NewSentinel("resource not found")

	// Create an error and classify it
	err := errors.New("record missing")
	classifiedErr := errx.Classify(err, ErrNotFound)

	// Check the classification
	if errors.Is(classifiedErr, ErrNotFound) {
		fmt.Println("Error is classified as not found")
	}

}
Output:

Error is classified as not found
Example (ApiHandler)

Example_apiHandler demonstrates a practical API error handling pattern

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	// Define error tags
	ErrNotFound := errx.NewSentinel("not found")
	ErrValidation := errx.NewSentinel("validation")

	// Simulate an error from the service layer
	var serviceErr error
	serviceErr = errx.NewDisplayable("Email is required")
	serviceErr = errx.Classify(serviceErr, ErrValidation)
	serviceErr = errx.Classify(serviceErr, errx.WithAttrs("field", "email"))

	// API handler logic
	statusCode := 500
	if errors.Is(serviceErr, ErrNotFound) {
		statusCode = 404
	} else if errors.Is(serviceErr, ErrValidation) {
		statusCode = 400
	}

	message := "An error occurred"
	if errx.IsDisplayable(serviceErr) {
		message = errx.DisplayText(serviceErr)
	}

	fmt.Printf("HTTP %d: %s\n", statusCode, message)

	// Log with attributes
	if errx.HasAttrs(serviceErr) {
		attrs := errx.ExtractAttrs(serviceErr)
		fmt.Printf("Attributes: %v\n", attrs)
	}

}
Output:

HTTP 400: Email is required
Attributes: [field=email]
Example (ApiHandlerWithDefault)

Example_apiHandlerWithDefault demonstrates using DisplayTextDefault in an API handler

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	// Define error sentinels
	ErrNotFound := errx.NewSentinel("not found")
	ErrDatabase := errx.NewSentinel("database")

	// Simulate different error scenarios
	type errorCase struct {
		name string
		err  error
	}

	cases := []errorCase{
		{
			name: "displayable error",
			err:  errx.Wrap("user lookup failed", errx.NewDisplayable("User not found"), ErrNotFound),
		},
		{
			name: "internal error",
			err:  errx.Wrap("query failed", errors.New("connection timeout"), ErrDatabase),
		},
	}

	for _, tc := range cases {
		// Using DisplayTextDefault provides consistent fallback behavior
		message := errx.DisplayTextDefault(tc.err, "An unexpected error occurred")
		fmt.Printf("%s: %s\n", tc.name, message)
	}

}
Output:

displayable error: User not found
internal error: An unexpected error occurred
Example (CombinedUsage)

Example_combinedUsage demonstrates combining all features

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	// Define tags
	ErrNotFound := errx.NewSentinel("not found")

	// Create error with attributes
	baseErr := errors.New("record not found in database")
	attrErr := errx.WithAttrs("table", "users", "id", 123)

	// Classify the error
	classifiedErr := errx.Classify(baseErr, attrErr, ErrNotFound)

	// Add displayable message
	displayErr := errx.NewDisplayable("User not found")
	finalErr := errx.Classify(classifiedErr, displayErr)

	// Check classification
	fmt.Println("Is not found:", errors.Is(finalErr, ErrNotFound))

	// Get displayable message
	fmt.Println("Display:", errx.DisplayText(finalErr))

	// Extract attributes
	attrs := errx.ExtractAttrs(finalErr)
	fmt.Printf("Attributes: %d found\n", len(attrs))

}
Output:

Is not found: true
Display: User not found
Attributes: 2 found
Example (ErrorChain)

Example_errorChain demonstrates working with error chains

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	ErrValidation := errx.NewSentinel("validation")

	// Build an error chain
	err1 := errors.New("field is empty")
	err2 := errx.Classify(err1, ErrValidation)
	err3 := errx.Wrap("validation failed", err2)
	err4 := fmt.Errorf("request processing: %w", err3)

	// Check classification through the chain
	fmt.Println("Is validation error:", errors.Is(err4, ErrValidation))

	// Add displayable at any level
	displayErr := errx.NewDisplayable("Please provide a valid value")
	err5 := errx.Classify(err4, displayErr)

	// Display message found through chain
	fmt.Println("Display text:", errx.DisplayText(err5))
	fmt.Println("Full error:", err5.Error())

}
Output:

Is validation error: true
Display text: Please provide a valid value
Full error: request processing: validation failed: field is empty
Example (RichError)

Example_richError demonstrates creating a fully-featured error

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	// Define classification hierarchy
	ErrDatabase := errx.NewSentinel("database")
	ErrRetryable := errx.NewSentinel("retryable")
	ErrDBTimeout := errx.NewSentinel("db timeout", ErrDatabase, ErrRetryable)

	// Create base error
	baseErr := errors.New("connection timeout after 30s")

	// Add user-facing message
	displayErr := errx.NewDisplayable("The service is temporarily unavailable")

	// Add structured context
	attrErr := errx.WithAttrs(
		"database", "users",
		"operation", "read",
		"timeout_seconds", 30,
	)

	// Combine everything
	finalErr := errx.Wrap("query execution failed", baseErr, displayErr, attrErr, ErrDBTimeout)

	// Use the error
	fmt.Println("Classification checks:")
	fmt.Println("  Is database error:", errors.Is(finalErr, ErrDatabase))
	fmt.Println("  Is retryable:", errors.Is(finalErr, ErrRetryable))

	fmt.Println("\nUser message:", errx.DisplayText(finalErr))

	fmt.Println("\nLogging context:")
	if errx.HasAttrs(finalErr) {
		attrs := errx.ExtractAttrs(finalErr)
		for _, attr := range attrs {
			fmt.Printf("  %s: %v\n", attr.Key, attr.Value)
		}
	}

	fmt.Println("\nFull error:", finalErr.Error())

}
Output:

Classification checks:
  Is database error: true
  Is retryable: true

User message: The service is temporarily unavailable

Logging context:
  database: users
  operation: read
  timeout_seconds: 30

Full error: query execution failed: connection timeout after 30s

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Classify

func Classify(cause error, classifications ...Classified) error

Classify attaches one or more classification sentinels to an existing error. The attached classification sentinels can be used later to identify the error using errors.Is. If err is nil, Classify returns nil.

Example:

var ErrNotFound = errx.NewSentinel("resource not found")

baseErr := errors.New("resource missing")
classifiedErr := errx.Classify(baseErr, ErrNotFound)

fmt.Println(errors.Is(classifiedErr, ErrNotFound)) // Output: true
Example

ExampleClassify demonstrates classifying errors without adding context

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	ErrValidation := errx.NewSentinel("validation error")

	err := errors.New("invalid email format")
	classified := errx.Classify(err, ErrValidation)

	fmt.Println(classified.Error())
	fmt.Println("Is validation error:", errors.Is(classified, ErrValidation))

}
Output:

invalid email format
Is validation error: true

func DisplayText

func DisplayText(err error) string

DisplayText extracts the first displayable error message from an error chain. If a displayable error is found anywhere in the error chain (using errors.As), it returns just the displayable error's message without any wrapper context. If no displayable error is found, it returns the full error message.

If multiple displayable errors exist in the chain, the message returned is the first one discovered via error traversal. This selection is based on the traversal order and does not imply any precedence semantics.

This is useful for APIs that need to return user-friendly error messages while maintaining detailed error context internally.

Example:

displayErr := NewDisplayable("Resource not found")
wrapped := Wrap("failed to fetch resource", displayErr, ErrNotFound)
deepWrapped := fmt.Errorf("operation failed: %w", wrapped)

// Returns: "Resource not found" (extracts just the displayable message)
msg := DisplayText(deepWrapped)

// For errors without displayable messages, returns full message
regularErr := errors.New("internal error")
msg := DisplayText(regularErr)  // Returns: "internal error"
Example

ExampleDisplayText demonstrates extracting displayable messages

package main

import (
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	displayErr := errx.NewDisplayable("Invalid email address")
	wrapped := errx.Wrap("validation failed", displayErr)

	// Extract just the displayable message
	msg := errx.DisplayText(wrapped)
	fmt.Println("Display message:", msg)

	// Full error for logging
	fmt.Println("Full error:", wrapped.Error())

}
Output:

Display message: Invalid email address
Full error: validation failed: Invalid email address

func DisplayTextDefault

func DisplayTextDefault(err error, def string) string

DisplayTextDefault extracts the first displayable error message from an error chain, or returns a default message if no displayable error is found.

This function behaves like DisplayText, but instead of returning the full error message when no displayable error is found, it returns the provided default message. This is useful for providing consistent, user-friendly fallback messages.

If err is nil, it returns an empty string (not the default message).

Example:

// Error with displayable message
displayErr := NewDisplayable("Invalid email format")
wrapped := Wrap("validation failed", displayErr)
msg := DisplayTextDefault(wrapped, "An error occurred")
// Returns: "Invalid email format"

// Error without displayable message
regularErr := errors.New("database connection timeout")
msg := DisplayTextDefault(regularErr, "Service temporarily unavailable")
// Returns: "Service temporarily unavailable"

// Nil error
msg := DisplayTextDefault(nil, "An error occurred")
// Returns: ""
Example

ExampleDisplayTextDefault demonstrates extracting displayable messages with fallback

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	// Error with displayable message - returns the displayable message
	displayErr := errx.NewDisplayable("Invalid email address")
	wrapped := errx.Wrap("validation failed", displayErr)
	msg1 := errx.DisplayTextDefault(wrapped, "An error occurred")
	fmt.Println("With displayable:", msg1)

	// Error without displayable message - returns the default
	regularErr := errors.New("database connection timeout")
	msg2 := errx.DisplayTextDefault(regularErr, "Service temporarily unavailable")
	fmt.Println("Without displayable:", msg2)

	// Nil error - returns empty string
	msg3 := errx.DisplayTextDefault(nil, "Default message")
	fmt.Printf("Nil error: %q\n", msg3)

}
Output:

With displayable: Invalid email address
Without displayable: Service temporarily unavailable
Nil error: ""

func HasAttrs

func HasAttrs(err error) bool

HasAttrs checks if an error contains structured attributes. It returns true if the error or any wrapped error is an attributed error.

Example

ExampleHasAttrs demonstrates checking if an error has attributes

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	attrErr := errx.WithAttrs("key", "value")
	regularErr := errors.New("no attributes")

	fmt.Println("Attr error has attrs:", errx.HasAttrs(attrErr))
	fmt.Println("Regular error has attrs:", errx.HasAttrs(regularErr))

}
Output:

Attr error has attrs: true
Regular error has attrs: false

func IsDisplayable

func IsDisplayable(err error) bool

IsDisplayable reports whether any error in err's chain is a displayable error. It traverses the error chain using errors.As to find a displayable error.

This is useful for conditionally handling displayable errors differently from internal errors.

Example:

if IsDisplayable(err) {
    // Safe to display to user
    return DisplayText(err)
}
// Internal error, log details but show generic message
log.Error(err)
return "An error occurred"
Example

ExampleIsDisplayable demonstrates checking if an error has a displayable message

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	displayErr := errx.NewDisplayable("Operation failed")
	regularErr := errors.New("internal error")

	fmt.Println("Display error is displayable:", errx.IsDisplayable(displayErr))
	fmt.Println("Regular error is displayable:", errx.IsDisplayable(regularErr))

}
Output:

Display error is displayable: true
Regular error is displayable: false

func Wrap

func Wrap(text string, cause error, classifications ...Classified) error

Wrap wraps an error with additional context text and optional classification sentinels. The attached classification sentinels can be used later to identify the error using errors.Is, as well as add displayable errors. If err is nil, Wrap returns nil.

If no classifications are provided, Wrap behaves like fmt.Errorf with %w, avoiding unnecessary carrier allocation.

Example

ExampleWrap demonstrates wrapping errors with context and tags

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	ErrDatabase := errx.NewSentinel("database error")

	err := errors.New("connection timeout")
	wrapped := errx.Wrap("failed to query database", err, ErrDatabase)

	fmt.Println(wrapped.Error())
	fmt.Println("Is database error:", errors.Is(wrapped, ErrDatabase))

}
Output:

failed to query database: connection timeout
Is database error: true

Types

type Attr

type Attr struct {
	Key   string
	Value any
}

Attr represents a key-value pair for structured error context.

func ExtractAttrs

func ExtractAttrs(err error) []Attr

ExtractAttrs extracts and merges all structured attributes from an error chain. It traverses the entire error chain and collects attributes from all attributed instances.

The order of attributes in the result is stable for a given error graph, but this ordering is not a semantic guarantee. Callers should not rely on attribute ordering for precedence or any other logic. If you need a map with specific merge semantics, consider converting the result to a map with your own collision-handling rules.

Returns nil if the error is nil or does not contain any attributes.

Example

ExampleExtractAttrs demonstrates extracting attributes from nested errors

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	// Create error with attributes
	baseErr := errors.New("database connection failed")
	attrErr := errx.WithAttrs("host", "localhost", "port", 5432)
	classified := errx.Classify(baseErr, attrErr)

	// Wrap it further
	wrapped := fmt.Errorf("startup failed: %w", classified)

	// Extract attributes from anywhere in the chain
	attrs := errx.ExtractAttrs(wrapped)
	fmt.Printf("Extracted %d attributes\n", len(attrs))
	for _, attr := range attrs {
		fmt.Printf("%s: %v\n", attr.Key, attr.Value)
	}

}
Output:

Extracted 2 attributes
host: localhost
port: 5432

func (Attr) String

func (a Attr) String() string

String returns a string representation of the Attr.

type AttrMap

type AttrMap = map[string]any

type Attrs

type Attrs []Attr

Attrs is a slice of Attr structs.

func (Attrs) String

func (al Attrs) String() string

String returns a string representation of the Attrs slice.

type Classified

type Classified interface {
	error
	// IsClassified is a marker method that identifies this error as a Classified error.
	// It should always return true for valid Classified implementations.
	// This method allows programmatic distinction between regular errors and errx Classified errors.
	IsClassified() bool
}

Classified is an interface for errors that can be classified. This interface can be implemented by external packages to extend the library. Internally, there are four categories of Classified implementations:

  1. Sentinel errors (*sentinel): Pure markers for programmatic error checking using errors.Is.

  2. Displayable errors (*displayable): Errors with messages safe to display to end users.

  3. Attributed errors (*attributed): Errors that carry structured metadata (key-value pairs) for logging and debugging.

  4. Traced errors (stacktrace.*traced): Errors that capture stack traces (in stacktrace subpackage).

The IsClassified() method serves as a type marker to distinguish Classified errors from regular Go errors. All implementations should return true.

func FromAttrMap

func FromAttrMap(attrMap AttrMap) Classified

FromAttrMap creates an error from a map of attributes. This is a convenience function for creating attributed errors from existing maps.

Order Non-Determinism

WARNING: The order of attributes in the resulting error is non-deterministic because Go map iteration order is randomized. If you need deterministic ordering, use WithAttrs with a slice of Attr instead:

// Non-deterministic order
err := errx.FromAttrMap(map[string]any{"key1": "val1", "key2": "val2"})

// Deterministic order
err := errx.WithAttrs(
    errx.Attr{Key: "key1", Value: "val1"},
    errx.Attr{Key: "key2", Value: "val2"},
)
Example

ExampleFromAttrMap demonstrates creating attributes from a map

package main

import (
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	attrs := map[string]any{
		"user_id":  42,
		"ip":       "192.168.1.1",
		"endpoint": "/api/users",
	}

	attrErr := errx.FromAttrMap(attrs)
	extracted := errx.ExtractAttrs(attrErr)

	fmt.Printf("Total attributes: %d\n", len(extracted))
	fmt.Println("Has attrs:", errx.HasAttrs(attrErr))

}
Output:

Total attributes: 3
Has attrs: true

func NewDisplayable

func NewDisplayable(message string) Classified

NewDisplayable creates a new displayable error with the given message. Displayable errors are intended for error messages that should be displayed to end users. These errors can be extracted from an error chain using DisplayText.

Example:

err := NewDisplayable("Invalid email address")
wrapped := fmt.Errorf("validation failed: %w", err)
msg := DisplayText(wrapped)  // Returns: "Invalid email address"
Example

ExampleNewDisplayable demonstrates creating displayable errors

package main

import (
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	displayErr := errx.NewDisplayable("User not found")

	fmt.Println(displayErr.Error())
	fmt.Println("Is displayable:", errx.IsDisplayable(displayErr))

}
Output:

User not found
Is displayable: true

func NewSentinel

func NewSentinel(text string, parents ...Classified) Classified

NewSentinel creates a new classification sentinel with the given text. Classification sentinels are used for programmatic error checking with errors.Is. The sentinel text is intentionally not visible in error message chains.

Optional parent sentinels can be provided to create a hierarchy. A sentinel with parents will match itself and all of its parents via errors.Is.

Circular References

WARNING: Creating circular parent references will cause infinite loops when using errors.Is. It is the caller's responsibility to avoid circular hierarchies. For example:

// DON'T DO THIS - creates a circular reference
parent := errx.NewSentinel("parent")
child := errx.NewSentinel("child", parent)
// Then somehow making parent reference child would create a cycle

The package does not detect or prevent circular references for performance reasons. Always ensure your sentinel hierarchies form a directed acyclic graph (DAG).

Example:

// Simple sentinel
ErrDatabase := errx.NewSentinel("database error")

// Sentinel with parent (hierarchical)
ErrTimeout := errx.NewSentinel("timeout", ErrDatabase)
// Now ErrTimeout will match both itself and ErrDatabase

// Sentinel with multiple parents
ErrCritical := errx.NewSentinel("critical")
ErrDatabaseCritical := errx.NewSentinel("critical database error", ErrDatabase, ErrCritical)
// Matches itself, ErrDatabase, and ErrCritical
Example (MultipleParents)

ExampleNewTag_multipleParents demonstrates creating a tag with multiple parent tags

package main

import (
	"errors"
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	// Create independent classification dimensions
	ErrRetryable := errx.NewSentinel("retryable")
	ErrDatabase := errx.NewSentinel("database")

	// Create a tag that inherits from both
	ErrDatabaseTimeout := errx.NewSentinel("database timeout", ErrDatabase, ErrRetryable)

	err := errx.Wrap("query failed", errors.New("connection timeout"), ErrDatabaseTimeout)

	// Can check for specific error
	if errors.Is(err, ErrDatabaseTimeout) {
		fmt.Println("Specific: database timeout")
	}

	// Can check for database errors
	if errors.Is(err, ErrDatabase) {
		fmt.Println("Category: database error")
	}

	// Can check for retryable errors
	if errors.Is(err, ErrRetryable) {
		fmt.Println("Behavior: retryable error")
	}

}
Output:

Specific: database timeout
Category: database error
Behavior: retryable error

func WithAttrs

func WithAttrs(attrs ...any) Classified

WithAttrs creates an error with structured attributes for additional context. Attributes can be extracted later using ExtractAttrs.

WithAttrs is typically used in combination with Wrap or Classify to create rich errors with both meaningful error messages and structured metadata:

// RECOMMENDED: Combine with Wrap for context + attributes
attrErr := errx.WithAttrs("user_id", 123, "action", "delete")
return errx.Wrap("failed to delete user", baseErr, attrErr)

// RECOMMENDED: Combine with Classify for classification + attributes
attrErr := errx.WithAttrs("retry_count", 3)
return errx.Classify(baseErr, ErrRetryable, attrErr)

Using WithAttrs alone produces a less informative error message that only shows the attribute list. For better error messages, always combine it with Wrap or Classify.

Input Formats

WithAttrs accepts multiple input formats:

  • Key-value pairs: WithAttrs("key1", value1, "key2", value2)
  • Attr structs: WithAttrs(Attr{Key: "key1", Value: value1}, Attr{Key: "key2", Value: value2})
  • Attr slices: WithAttrs([]Attr{{Key: "key1", Value: value1}, {Key: "key2", Value: value2}})
  • Mixed formats: WithAttrs("key1", value1, Attr{Key: "key2", Value: value2})

The arguments are processed following a structured pattern (similar to slog):

  • If an argument is an Attr, it is used as is.
  • If an argument is an []Attr or Attrs, all attributes are appended.
  • If an argument is a string and this is not the last argument, the following argument is treated as the value and the two are combined into an Attr.
  • Otherwise, the argument is treated as a value with key "!BADKEY".

The "!BADKEY" key is used for malformed arguments to help identify issues during debugging. This behavior matches the slog package's handling of malformed key-value pairs.

Examples:

WithAttrs("key", "value")                    // Normal key-value pair
WithAttrs("key")                             // Odd number: Attr{Key: "!BADKEY", Value: "key"}
WithAttrs(123)                               // Non-string: Attr{Key: "!BADKEY", Value: 123}
WithAttrs("key", 123)                        // String key with int value: Attr{Key: "key", Value: 123}
WithAttrs(Attr{Key: "k", Value: "v"})        // Direct Attr usage
WithAttrs([]Attr{{Key: "k", Value: "v"}})    // Slice of Attrs
Example

ExampleWithAttrs demonstrates adding structured attributes to errors

package main

import (
	"fmt"

	"github.com/go-extras/errx"
)

func main() {
	attrErr := errx.WithAttrs(
		"user_id", 12345,
		"action", "delete",
		"resource", "account",
	)

	attrs := errx.ExtractAttrs(attrErr)
	for _, attr := range attrs {
		fmt.Printf("%s=%v ", attr.Key, attr.Value)
	}

}
Output:

user_id=12345 action=delete resource=account

Directories

Path Synopsis
Package json provides JSON serialization capabilities for errx errors.
Package json provides JSON serialization capabilities for errx errors.
Package stacktrace provides optional stack trace support for errx errors.
Package stacktrace provides optional stack trace support for errx errors.

Jump to

Keyboard shortcuts

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