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 ¶
- func Classify(cause error, classifications ...Classified) error
- func DisplayText(err error) string
- func DisplayTextDefault(err error, def string) string
- func HasAttrs(err error) bool
- func IsDisplayable(err error) bool
- func Wrap(text string, cause error, classifications ...Classified) error
- type Attr
- type AttrMap
- type Attrs
- type Classified
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Attr represents a key-value pair for structured error context.
func ExtractAttrs ¶
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
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:
Sentinel errors (*sentinel): Pure markers for programmatic error checking using errors.Is.
Displayable errors (*displayable): Errors with messages safe to display to end users.
Attributed errors (*attributed): Errors that carry structured metadata (key-value pairs) for logging and debugging.
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.
Recommended Usage ¶
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. |