Documentation
¶
Overview ¶
Package errx provides structured error handling with standardized error codes, rich context for debugging, and safe client messaging.
errx is an error handling package that provides:
- Standardized Error Codes: Typed error codes for consistent error classification
- Three-Tier Error Messages:
- System components: Error codes for inter-service communication
- System maintainers: Rich debug information with stack traces, metadata, and implementation details
- Clients: Safe error messages without exposing internal details
- Transport Agnostic: Works with HTTP, gRPC, or any other transport layer
- Go Ecosystem Integration: Full support for errors.Is, errors.As, and errors.Unwrap
- Builder API: Fluent, chainable methods for error construction
- Automatic Stack Traces: Captures stack traces for debugging
- Error Wrapping: Chain errors while preserving context
Quick Start ¶
Creating Errors:
// Simple error
err := errx.New(errx.CodeNotFound, "user not found")
// Formatted error
err := errx.Newf(errx.CodeInvalidArgument, "invalid user ID: %d", userID)
// Wrapping existing errors
dbErr := errors.New("connection refused")
err := errx.Wrap(dbErr, errx.CodeUnavailable, "database unavailable")
Adding Context:
err := errx.New(errx.CodePermissionDenied, "access denied").
WithDetail("resource", "admin-panel"). // Client-safe details
WithSource("auth-service"). // Set source (service/package/component)
WithTags("security", "rbac"). // Add tags for categorization
WithMeta("user_id", userID). // Internal metadata
WithDebug("user missing admin role"). // Internal debug message
WithRetryable() // Mark as retryable operation
Using Errors:
// Get error code
errCode := err.Code() // Returns errx.Code
codeStr := err.Code().String() // Returns "permission_denied"
// Get client-safe data
clientMsg := err.Error() // "access denied"
details := err.Details() // map[string]any{"resource": "admin-panel"}
// Get internal data
source := err.Source() // "auth-service"
tags := err.Tags() // []string{"security", "rbac"}
metadata := err.Metadata() // map[string]any{"user_id": 123}
// Get debug message (for logging/debugging)
debugMsg := err.DebugMessage() // "[permission_denied] access denied | class=access-denied | ..."
// Get stack trace
stackTrace := err.FormatStackTrace() // Human-readable stack trace
Error Codes ¶
The package provides strictly defined standardized error codes. All codes are constants to ensure consistency:
CodeUnknown // Unknown error (default/zero value) CodeCanceled // Operation canceled by caller CodeInvalidArgument // Request is invalid, regardless of system state CodeDeadlineExceeded // Deadline expired before operation completed CodeNotFound // Requested resource cannot be found CodeAlreadyExists // Resource already exists CodePermissionDenied // Caller isn't authorized CodeResourceExhausted // Resource exhausted (quota, storage, etc.) CodeFailedPrecondition // System isn't in required state CodeAborted // Operation aborted (concurrency issue) CodeOutOfRange // Operation attempted past valid range CodeUnimplemented // Operation not implemented/supported CodeInternal // Internal error (invariant broken) CodeUnavailable // Service temporarily unavailable CodeDataLoss // Unrecoverable data loss or corruption CodeUnauthenticated // Valid authentication credentials required
These error codes align with the Connect RPC protocol specification.
Context-Based Metadata ¶
Use WithMetaContext to store request-scoped metadata in a context, then attach it to errors with WithMetaFromContext:
ctx = errx.WithMetaContext(ctx, "user_id", 123, "action", "delete")
// Later, when creating an error:
err := errx.New(errx.CodeNotFound, "user not found").WithMetaFromContext(ctx)
// err.Metadata() == map[string]any{"user_id": 123, "action": "delete"}
Metadata accumulates across calls:
ctx = errx.WithMetaContext(ctx, "user_id", 123) ctx = errx.WithMetaContext(ctx, "action", "delete") // Both "user_id" and "action" are present
WithMetaFromContext uses last-write-wins: if the same key was set via WithMeta, the context value takes precedence. Reverse the call order to give WithMeta priority.
Ensure Functions ¶
Use Ensure and Ensuref to guarantee an error is an *Error without clobbering the original code. If the error is already an *Error (or wraps one), it is returned unchanged. Otherwise, it is wrapped with the given fallback code and message:
// At a service boundary — preserves not_found, only wraps unknown errors as internal return errx.Ensure(err, errx.CodeInternal, "unexpected error") // With formatting return errx.Ensuref(err, errx.CodeInternal, "unexpected error in %s", "user-service")
Convenience variants exist for each code:
return errx.EnsureInternal(err, "unexpected error") return errx.EnsurefInternal(err, "unexpected error in %s", "user-service")
Convenience Functions ¶
For each error code, the package provides convenience constructors:
errx.New{Code}(msg) // e.g., errx.NewNotFound("user not found")
errx.Newf{Code}(format, args...) // e.g., errx.NewfNotFound("user %d not found", id)
errx.Wrap{Code}(err, msg) // e.g., errx.WrapInternal(err, "db failed")
errx.Wrapf{Code}(err, format, args...) // e.g., errx.WrapfInternal(err, "db %s failed", name)
errx.Ensure{Code}(err, msg) // e.g., errx.EnsureInternal(err, "unexpected")
errx.Ensuref{Code}(err, format, args...) // e.g., errx.EnsurefInternal(err, "unexpected in %s", svc)
Usage Patterns ¶
Layered Error Handling:
// Data layer
func (r *UserRepository) FindByID(id int) (*User, error) {
user, err := r.db.Query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, errx.Wrap(err, errx.CodeInternal, "database query failed").
WithSource("user-repository").
WithTags("database").
WithMeta("user_id", id).
WithDebugf("query failed: %v", err)
}
if user == nil {
return nil, errx.New(errx.CodeNotFound, "user not found").
WithSource("user-repository").
WithMeta("user_id", id)
}
return user, nil
}
// Service layer
func (s *UserService) GetUser(id int) (*User, error) {
user, err := s.repo.FindByID(id)
if err != nil {
if e, ok := errx.As(err); ok && e.Code() != errx.CodeNotFound {
return nil, err
}
user, err = s.repo.CreateDefaultUser(id)
if err != nil {
return nil, err
}
}
return user, nil
}
Working with Standard Errors:
// Check if error is an errx.Error
if errx.Is(err) {
// It's an errx error
}
// Extract errx.Error from any error
if e, ok := errx.As(err); ok {
code := e.Code()
source := e.Source()
metadata := e.Metadata()
}
// Check if error has a specific code
if errx.CodeIs(err, errx.CodeNotFound) {
// Handle not found
}
// Check if error has any of multiple codes
if errx.CodeIn(err, errx.CodeNotFound, errx.CodeUnauthenticated) {
// Handle client errors
}
// Extract error code
errCode := errx.CodeOf(err) // Returns CodeUnknown for non-errx errors
// Check if error is retryable
if errx.IsRetryable(err) {
// Retry the operation
}
// Also compatible with standard errors package
err1 := errx.New(errx.CodeNotFound, "not found")
err2 := errx.New(errx.CodeNotFound, "different message")
errors.Is(err1, err2) // true - same code
var errxErr *errx.Error
if errors.As(err, &errxErr) {
code := errxErr.Code()
}
Generic Type Checking ¶
For most use cases, use the non-generic Is() and As() functions to work with *errx.Error. For checking other custom error types, use the generic IsType[E] and AsType[E] functions:
// Non-generic versions for errx.Error (recommended for common case)
if errx.Is(err) {
// err is or wraps an *errx.Error
}
if e, ok := errx.As(err); ok {
// e is an *errx.Error
code := e.Code()
}
// Generic versions for custom error types
type MyCustomError struct {
Reason string
}
func (e *MyCustomError) Error() string {
return e.Reason
}
// Check if error is or wraps a custom type
if errx.IsType[*MyCustomError](err) {
// err is or wraps a *MyCustomError
}
// Extract custom error type from error chain
if e, ok := errx.AsType[*MyCustomError](err); ok {
// e is a *MyCustomError
fmt.Println(e.Reason)
}
Marking Errors as Retryable ¶
Use WithRetryable() to indicate that an operation can be retried:
// Temporary service unavailability - can be retried
err := errx.New(errx.CodeUnavailable, "service temporarily unavailable").
WithRetryable().
WithSource("payment-service")
// Check if an error is retryable
if errx.IsRetryable(err) {
// Implement retry logic (e.g., return to retry queue, exponential backoff, etc.)
}
// Works with wrapped errors too
wrappedErr := fmt.Errorf("payment failed: %w", err)
errx.IsRetryable(wrappedErr) // true
Common retryable scenarios:
- CodeUnavailable: Service temporarily down
- CodeDeadlineExceeded: Request timeout
- CodeResourceExhausted: Rate limit exceeded
- CodeAborted: Optimistic locking conflict
Non-retryable errors typically include:
- CodeInvalidArgument: Bad request data
- CodeNotFound: Resource doesn't exist
- CodePermissionDenied: Authorization failure
- CodeUnimplemented: Feature not supported
Protecting Client-Facing Messages:
// BAD: Exposing implementation details
err := errx.New(errx.CodeInternal, "failed to connect to postgres.internal.company.com:5432")
// GOOD: Safe client message + debug details
err := errx.New(errx.CodeInternal, "service temporarily unavailable").
WithDebug("failed to connect to postgres.internal.company.com:5432").
WithMeta("db_query", "SELECT * FROM users WHERE id = ?")
// Client sees: "service temporarily unavailable" (Error())
// Logs contain: full debug message with all details
Client vs. Internal Data ¶
The error package separates data into two categories:
Client-Exposed Data (Safe to Return to End Users):
- Error(): Human-readable error message safe for clients (standard error interface)
- Details(): Key-value pairs safe to expose (e.g., {"resource": "admin-panel"})
Internal-Only Data (For Debugging/Logging):
- Source(): Source (service/package/component) where error occurred (e.g., "user-service")
- Tags(): Categorization tags (e.g., ["database", "critical"])
- Metadata(): Debug key-value pairs (e.g., {"db_host": "postgres.internal", "user_id": 123})
- DebugMessage(): Full debug message with all context
Best Practices ¶
1. Use Safe Client Messages: Never expose implementation details in the error message
// Bad
err := errx.New(errx.CodeInternal, "SQL: connection to db.internal failed")
// Good
err := errx.New(errx.CodeInternal, "service unavailable").
WithDebug("SQL: connection to db.internal failed")
2. Add Context at Each Layer: Each layer should add relevant context
// Repository layer
err = errx.Wrap(err, code, msg).WithSource("user-repo").WithTags("database")
// Service layer
err = errx.Wrap(err, code, msg).WithSource("user-service").WithMeta("user_id", id)
3. Use Appropriate Error Codes: Choose codes that accurately represent the error
// Not found vs. permission denied
if user == nil {
return errx.New(errx.CodeNotFound, "user not found") // User doesn't exist
}
if !canAccess {
return errx.New(errx.CodePermissionDenied, "access denied") // User exists but no permission
}
4. Leverage Metadata for Debugging: Add useful debugging information
err.WithMeta("user_id", id).
WithMeta("action", "delete").
WithMeta("retry_count", retries)
5. Don't Create Empty Error Chains: Only wrap when you have context to add
// Bad - adds no value
return errx.Wrap(err, errx.CodeInternal, err.Error())
// Good - adds context
return errx.Wrap(err, errx.CodeInternal, "failed to process payment").
WithMeta("payment_id", paymentID)
Testing ¶
Example test using errx errors:
func TestUserService_GetUser_NotFound(t *testing.T) {
// ... setup
user, err := service.GetUser(999)
require.Error(t, err)
assert.Nil(t, user)
// Check if it's an errx error
assert.True(t, errx.Is(err))
// Extract and verify details
e, ok := errx.As(err)
require.True(t, ok)
assert.Equal(t, errx.CodeNotFound, e.Code())
assert.Equal(t, "user-service", e.Source())
// Or use code helpers
assert.True(t, errx.CodeIs(err, errx.CodeNotFound))
assert.Equal(t, errx.CodeNotFound, errx.CodeOf(err))
assert.True(t, errx.CodeIn(err, errx.CodeNotFound, errx.CodeUnauthenticated))
}
Educational Resources ¶
Error Handling as Domain Design ¶
Failure is your Domain by Ben Johnson (https://web.archive.org/web/2020/https://middlemost.com/failure-is-your-domain/)
This article advocates treating errors as part of your application's domain rather than as afterthoughts. It introduces a three-consumer model for errors:
1. Application: Needs machine-readable error codes for programmatic handling and inter-service communication
2. End User: Needs human-readable messages that are safe to display (without exposing implementation details)
3. Operator: Needs rich debugging context including logical stack traces, metadata, and internal details
The errx package implements these principles with its standardized error codes (aligned with ConnectRPC), three-tier messaging system (Code/Error()/Details for clients, Source/Tags/Metadata/DebugMessage for operators), and automatic stack trace capture.
Index ¶
- func AsType[E error](err error) (E, bool)
- func CodeIn(err error, codes ...Code) bool
- func CodeIs(err error, code Code) bool
- func CodeNames() []string
- func Is(err error) bool
- func IsRetryable(err error) bool
- func IsType[E error](err error) bool
- func WithMetaContext(ctx context.Context, keyvals ...any) context.Context
- type Code
- type Error
- func As(err error) (*Error, bool)
- func Ensure(err error, code Code, message string) *Error
- func EnsureAborted(err error, msg string) *Error
- func EnsureAlreadyExists(err error, msg string) *Error
- func EnsureCanceled(err error, msg string) *Error
- func EnsureDataLoss(err error, msg string) *Error
- func EnsureDeadlineExceeded(err error, msg string) *Error
- func EnsureFailedPrecondition(err error, msg string) *Error
- func EnsureInternal(err error, msg string) *Error
- func EnsureInvalidArgument(err error, msg string) *Error
- func EnsureNotFound(err error, msg string) *Error
- func EnsureOutOfRange(err error, msg string) *Error
- func EnsurePermissionDenied(err error, msg string) *Error
- func EnsureResourceExhausted(err error, msg string) *Error
- func EnsureUnauthenticated(err error, msg string) *Error
- func EnsureUnavailable(err error, msg string) *Error
- func EnsureUnimplemented(err error, msg string) *Error
- func EnsureUnknown(err error, msg string) *Error
- func Ensuref(err error, code Code, format string, args ...any) *Error
- func EnsurefAborted(err error, format string, args ...any) *Error
- func EnsurefAlreadyExists(err error, format string, args ...any) *Error
- func EnsurefCanceled(err error, format string, args ...any) *Error
- func EnsurefDataLoss(err error, format string, args ...any) *Error
- func EnsurefDeadlineExceeded(err error, format string, args ...any) *Error
- func EnsurefFailedPrecondition(err error, format string, args ...any) *Error
- func EnsurefInternal(err error, format string, args ...any) *Error
- func EnsurefInvalidArgument(err error, format string, args ...any) *Error
- func EnsurefNotFound(err error, format string, args ...any) *Error
- func EnsurefOutOfRange(err error, format string, args ...any) *Error
- func EnsurefPermissionDenied(err error, format string, args ...any) *Error
- func EnsurefResourceExhausted(err error, format string, args ...any) *Error
- func EnsurefUnauthenticated(err error, format string, args ...any) *Error
- func EnsurefUnavailable(err error, format string, args ...any) *Error
- func EnsurefUnimplemented(err error, format string, args ...any) *Error
- func EnsurefUnknown(err error, format string, args ...any) *Error
- func New(code Code, message string) *Error
- func NewAborted(msg string) *Error
- func NewAlreadyExists(msg string) *Error
- func NewCanceled(msg string) *Error
- func NewDataLoss(msg string) *Error
- func NewDeadlineExceeded(msg string) *Error
- func NewFailedPrecondition(msg string) *Error
- func NewInternal(msg string) *Error
- func NewInvalidArgument(msg string) *Error
- func NewNotFound(msg string) *Error
- func NewOutOfRange(msg string) *Error
- func NewPermissionDenied(msg string) *Error
- func NewResourceExhausted(msg string) *Error
- func NewUnauthenticated(msg string) *Error
- func NewUnavailable(msg string) *Error
- func NewUnimplemented(msg string) *Error
- func NewUnknown(msg string) *Error
- func Newf(code Code, format string, args ...any) *Error
- func NewfAborted(format string, args ...any) *Error
- func NewfAlreadyExists(format string, args ...any) *Error
- func NewfCanceled(format string, args ...any) *Error
- func NewfDataLoss(format string, args ...any) *Error
- func NewfDeadlineExceeded(format string, args ...any) *Error
- func NewfFailedPrecondition(format string, args ...any) *Error
- func NewfInternal(format string, args ...any) *Error
- func NewfInvalidArgument(format string, args ...any) *Error
- func NewfNotFound(format string, args ...any) *Error
- func NewfOutOfRange(format string, args ...any) *Error
- func NewfPermissionDenied(format string, args ...any) *Error
- func NewfResourceExhausted(format string, args ...any) *Error
- func NewfUnauthenticated(format string, args ...any) *Error
- func NewfUnavailable(format string, args ...any) *Error
- func NewfUnimplemented(format string, args ...any) *Error
- func NewfUnknown(format string, args ...any) *Error
- func Wrap(err error, code Code, message string) *Error
- func WrapAborted(err error, msg string) *Error
- func WrapAlreadyExists(err error, msg string) *Error
- func WrapCanceled(err error, msg string) *Error
- func WrapDataLoss(err error, msg string) *Error
- func WrapDeadlineExceeded(err error, msg string) *Error
- func WrapFailedPrecondition(err error, msg string) *Error
- func WrapInternal(err error, msg string) *Error
- func WrapInvalidArgument(err error, msg string) *Error
- func WrapNotFound(err error, msg string) *Error
- func WrapOutOfRange(err error, msg string) *Error
- func WrapPermissionDenied(err error, msg string) *Error
- func WrapResourceExhausted(err error, msg string) *Error
- func WrapUnauthenticated(err error, msg string) *Error
- func WrapUnavailable(err error, msg string) *Error
- func WrapUnimplemented(err error, msg string) *Error
- func WrapUnknown(err error, msg string) *Error
- func Wrapf(err error, code Code, format string, args ...any) *Error
- func WrapfAborted(err error, format string, args ...any) *Error
- func WrapfAlreadyExists(err error, format string, args ...any) *Error
- func WrapfCanceled(err error, format string, args ...any) *Error
- func WrapfDataLoss(err error, format string, args ...any) *Error
- func WrapfDeadlineExceeded(err error, format string, args ...any) *Error
- func WrapfFailedPrecondition(err error, format string, args ...any) *Error
- func WrapfInternal(err error, format string, args ...any) *Error
- func WrapfInvalidArgument(err error, format string, args ...any) *Error
- func WrapfNotFound(err error, format string, args ...any) *Error
- func WrapfOutOfRange(err error, format string, args ...any) *Error
- func WrapfPermissionDenied(err error, format string, args ...any) *Error
- func WrapfResourceExhausted(err error, format string, args ...any) *Error
- func WrapfUnauthenticated(err error, format string, args ...any) *Error
- func WrapfUnavailable(err error, format string, args ...any) *Error
- func WrapfUnimplemented(err error, format string, args ...any) *Error
- func WrapfUnknown(err error, format string, args ...any) *Error
- func (e *Error) Code() Code
- func (e *Error) DebugMessage() string
- func (e *Error) Details() map[string]any
- func (e *Error) Error() string
- func (e *Error) FormatStackTrace() string
- func (e *Error) Is(target error) bool
- func (e *Error) IsRetryable() bool
- func (e *Error) LogValue() slog.Value
- func (e *Error) Metadata() map[string]any
- func (e *Error) Source() string
- func (e *Error) StackTrace() []uintptr
- func (e *Error) Tags() []string
- func (e *Error) Unwrap() error
- func (e *Error) WithDebug(message string) *Error
- func (e *Error) WithDebugf(format string, args ...any) *Error
- func (e *Error) WithDetail(key string, value any) *Error
- func (e *Error) WithMeta(key string, value any) *Error
- func (e *Error) WithMetaFromContext(ctx context.Context) *Error
- func (e *Error) WithRetryable() *Error
- func (e *Error) WithSource(source string) *Error
- func (e *Error) WithTags(tags ...string) *Error
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AsType ¶
AsType finds the first error in err's tree that matches the type E. Returns the error value and true if found, or zero value and false otherwise.
func CodeIn ¶
CodeIn checks if an error has a code matching any of the provided codes. It unwraps the error chain to find an *Error.
Example ¶
ExampleCodeIn demonstrates checking if error matches any of several codes.
package main
import (
"fmt"
"github.com/bjaus/errx"
)
func main() {
err := errx.New(errx.CodeNotFound, "resource not found")
// Check if error matches any of the provided codes
if errx.CodeIn(err, errx.CodeNotFound, errx.CodeUnauthenticated) {
fmt.Println("Client error occurred")
}
}
Output: Client error occurred
func CodeIs ¶
CodeIs checks if an error has a specific error code. It unwraps the error chain to find an *Error.
Example ¶
ExampleCodeIs demonstrates checking error codes.
package main
import (
"fmt"
"github.com/bjaus/errx"
)
func main() {
err := errx.New(errx.CodeNotFound, "resource not found")
// Check if error has specific code
if errx.CodeIs(err, errx.CodeNotFound) {
fmt.Println("Resource not found")
}
// Works with wrapped errors too
wrappedErr := fmt.Errorf("operation failed: %w", err)
if errx.CodeIs(wrappedErr, errx.CodeNotFound) {
fmt.Println("Still found through wrapper")
}
}
Output: Resource not found Still found through wrapper
func CodeNames ¶
func CodeNames() []string
CodeNames returns a list of possible string values of Code.
func IsRetryable ¶
IsRetryable checks if an error indicates a retryable operation. Returns false if the error is not an *Error.
func WithMetaContext ¶
WithMetaContext stores key-value metadata in the context for later attachment to errors via Error.WithMetaFromContext. It accepts alternating key-value pairs where each key should be a string. Non-string keys are silently skipped, and a trailing key with no value is silently dropped.
Each call copies the parent context's metadata into a new map, then applies the provided key-value pairs on top (last-write-wins). The parent's map is never mutated, so concurrent goroutines that derive from the same parent context each get an independent snapshot with the correct metadata for their scope.
Concurrency example:
ctx = errx.WithMetaContext(ctx, "request_id", reqID)
for _, postID := range postIDs {
group.Go(func() error {
ctx := errx.WithMetaContext(ctx, "post_id", postID)
// Each goroutine gets its own metadata snapshot.
// Errors created here carry post_id specific to this goroutine.
return errx.NewInternal("failed").WithMetaFromContext(ctx)
})
}
// Whatever error the errgroup returns will have the correct post_id
// for the goroutine that failed — no cross-contamination.
Basic usage:
ctx = errx.WithMetaContext(ctx, "user_id", 123, "action", "delete") err := errx.New(errx.CodeNotFound, "user not found").WithMetaFromContext(ctx)
Types ¶
type Code ¶
type Code uint8
Code represents a standardized error code. These codes provide a consistent error classification system that is transport-agnostic. Transport layers (HTTP, gRPC, etc.) can map these codes to their specific status codes.
ENUM( unknown, // Unknown error (default/zero value) canceled, // Operation canceled by caller invalid_argument, // Request is invalid, regardless of system state deadline_exceeded, // Deadline expired before operation completed not_found, // Requested resource cannot be found already_exists, // Resource already exists permission_denied, // Caller isn't authorized resource_exhausted, // Resource exhausted (quota, storage, etc.) failed_precondition, // System isn't in required state aborted, // Operation aborted (concurrency issue) out_of_range, // Operation attempted past valid range unimplemented, // Operation not implemented/supported internal, // Internal error (invariant broken) unavailable, // Service temporarily unavailable data_loss, // Unrecoverable data loss or corruption unauthenticated, // Valid authentication credentials required )
const ( // Unknown error (default/zero value) CodeUnknown Code = 0 // Operation canceled by caller CodeCanceled Code = 1 // Request is invalid, regardless of system state CodeInvalidArgument Code = 2 // Deadline expired before operation completed CodeDeadlineExceeded Code = 3 // Requested resource cannot be found CodeNotFound Code = 4 // Resource already exists CodeAlreadyExists Code = 5 // Caller isn't authorized CodePermissionDenied Code = 6 // Resource exhausted (quota, storage, etc.) CodeResourceExhausted Code = 7 // System isn't in required state CodeFailedPrecondition Code = 8 // Operation aborted (concurrency issue) CodeAborted Code = 9 // Operation attempted past valid range CodeOutOfRange Code = 10 // Operation not implemented/supported CodeUnimplemented Code = 11 // Internal error (invariant broken) CodeInternal Code = 12 CodeUnavailable Code = 13 // Unrecoverable data loss or corruption CodeDataLoss Code = 14 // Valid authentication credentials required CodeUnauthenticated Code = 15 )
func CodeOf ¶
CodeOf extracts the error code from an error. Returns CodeUnknown if the error is not an *Error.
Example ¶
ExampleCodeOf demonstrates extracting error code from any error.
package main
import (
"errors"
"fmt"
"github.com/bjaus/errx"
)
func main() {
// From errx error
err1 := errx.New(errx.CodeInvalidArgument, "bad input")
fmt.Println("errx error code:", errx.CodeOf(err1))
// From standard error (returns CodeUnknown)
err2 := errors.New("standard error")
fmt.Println("standard error code:", errx.CodeOf(err2))
// From wrapped errx error
err3 := fmt.Errorf("wrapped: %w", err1)
fmt.Println("wrapped error code:", errx.CodeOf(err3))
}
Output: errx error code: invalid_argument standard error code: unknown wrapped error code: invalid_argument
type Error ¶
type Error struct {
// contains filtered or unexported fields
}
Error represents a rich error with code, context, and debugging information. It implements the standard error interface and supports error wrapping.
Example (ClientVsDebug) ¶
Example test to demonstrate usage patterns
package main
import (
"errors"
"fmt"
"strings"
"github.com/bjaus/errx"
)
func main() {
// Simulating a database error in production
dbError := errors.New("pq: connection refused on host db.internal.company.com:5432")
// Wrap with safe client message
err := errx.Wrap(dbError, errx.CodeUnavailable, "Service temporarily unavailable").
WithSource("payment-service").
WithTags("database", "postgres").
WithDetail("service", "payment").
WithMeta("db_host", "db.internal.company.com").
WithMeta("retry_count", 3).
WithDebug("PostgreSQL connection pool exhausted after 3 retries")
// What the client sees
fmt.Println("Client sees:", err.Error())
// What gets logged for debugging
fmt.Println("Logs contain:", strings.Contains(err.DebugMessage(), "db.internal.company.com"))
}
Output: Client sees: Service temporarily unavailable Logs contain: true
Example (ClientVsInternalData) ¶
ExampleError_clientVsInternalData demonstrates the separation between client-safe and internal debugging data.
package main
import (
"fmt"
"github.com/bjaus/errx"
)
func main() {
// Create an error with both client-safe and internal data
err := errx.New(errx.CodeInternal, "service temporarily unavailable").
WithDetail("retry_after", "30s"). // Client sees this
WithSource("payment-service"). // Internal only
WithTags("external", "payment-provider"). // Internal only
WithMeta("transaction_id", "txn_123456"). // Internal only
WithMeta("retry_count", 3). // Internal only
WithDebug("payment provider returned 500 after retries") // Internal only
fmt.Println("=== Client-Safe Data (can be exposed in API responses) ===")
fmt.Println("Message:", err.Error())
// Print details field individually for deterministic output
details := err.Details()
fmt.Println("retry_after:", details["retry_after"])
fmt.Println("\n=== Internal Data (for logs/debugging only) ===")
fmt.Println("Source:", err.Source())
fmt.Println("Tags:", err.Tags())
// Print metadata fields individually for deterministic output
metadata := err.Metadata()
fmt.Println("transaction_id:", metadata["transaction_id"])
fmt.Println("retry_count:", metadata["retry_count"])
}
Output: === Client-Safe Data (can be exposed in API responses) === Message: service temporarily unavailable retry_after: 30s === Internal Data (for logs/debugging only) === Source: payment-service Tags: [external payment-provider] transaction_id: txn_123456 retry_count: 3
Example (LayeredArchitecture) ¶
ExampleError_layeredArchitecture demonstrates error handling through application layers.
package main
import (
"errors"
"fmt"
"github.com/bjaus/errx"
)
func main() {
// 1. Database layer error
dbErr := errors.New("connection refused")
// 2. Repository layer wraps and adds context
repoErr := errx.Wrap(dbErr, errx.CodeUnavailable, "database query failed").
WithSource("user-repository").
WithTags("database").
WithMeta("query", "SELECT * FROM users WHERE id = ?").
WithDebug("postgres connection pool exhausted")
// 3. Service layer wraps again with business context
serviceErr := errx.Wrap(repoErr, errx.CodeNotFound, "user not found").
WithSource("user-service").
WithDetail("user_id", "12345")
// Each layer sees its own context
fmt.Println("Service layer sees:", serviceErr.Error())
fmt.Println("Source:", serviceErr.Source())
// Can check for any error in the chain
fmt.Println("Contains DB error:", errors.Is(serviceErr, dbErr))
}
Output: Service layer sees: user not found Source: user-service Contains DB error: true
Example (SlogJSON) ¶
ExampleError_slogJSON demonstrates structured logging output with nested errors.
package main
import (
"log/slog"
"os"
"github.com/bjaus/errx"
)
func main() {
// Create a logger that outputs JSON
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelError,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
// Remove timestamp for consistent output
if a.Key == "time" {
return slog.Attr{}
}
return a
},
}))
// Scenario: Inner error from repository
innerErr := errx.New(errx.CodeInternal, "query execution failed").
WithSource("user-repository").
WithTags("database", "postgres").
WithMeta("query", "SELECT * FROM users").
WithDebug("deadlock detected")
// Outer error from service
outerErr := errx.Wrap(innerErr, errx.CodeNotFound, "user not found").
WithSource("user-service").
WithDetail("user_id", "12345").
WithMeta("request_id", "req-789")
// Log it
logger.Error("operation failed", "error", outerErr)
}
Output: {"level":"ERROR","msg":"operation failed","error":{"code":"not_found","message":"user not found","source":"user-service","details":{"user_id":"12345"},"metadata":{"request_id":"req-789"},"cause":{"code":"internal","message":"query execution failed","source":"user-repository","tags":["database","postgres"],"metadata":{"query":"SELECT * FROM users"},"debug":"deadlock detected"}}}
Example (SlogJSON_threeLevels) ¶
ExampleError_slogJSON_threeLevels demonstrates three-level nested error logging.
package main
import (
"log/slog"
"os"
"github.com/bjaus/errx"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelError,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == "time" {
return slog.Attr{}
}
return a
},
}))
// Level 1: Database driver
dbErr := errx.New(errx.CodeInternal, "connection refused").
WithSource("postgres-driver").
WithMeta("port", 5432)
// Level 2: Repository
repoErr := errx.Wrap(dbErr, errx.CodeUnavailable, "database unavailable").
WithSource("repository").
WithMeta("operation", "findByID")
// Level 3: Service
serviceErr := errx.Wrap(repoErr, errx.CodeNotFound, "resource not found").
WithSource("service").
WithMeta("resource_type", "user")
logger.Error("request failed", "error", serviceErr)
// Note how each layer is nested in the "cause" field, preserving all context
}
Output: {"level":"ERROR","msg":"request failed","error":{"code":"not_found","message":"resource not found","source":"service","metadata":{"resource_type":"user"},"cause":{"code":"unavailable","message":"database unavailable","source":"repository","metadata":{"operation":"findByID"},"cause":{"code":"internal","message":"connection refused","source":"postgres-driver","metadata":{"port":5432}}}}}
func As ¶
As finds the first *Error in err's tree. Returns the error value and true if found, or nil and false otherwise.
func Ensure ¶
Ensure guarantees the returned error is an *Error. If err is nil, returns nil. If err is already an *Error (or wraps one), returns the existing *Error unchanged. Otherwise, wraps err as the cause with the given code and message.
func EnsureAborted ¶
EnsureAborted ensures the error is an *Error, falling back to Aborted with the given message.
func EnsureAlreadyExists ¶
EnsureAlreadyExists ensures the error is an *Error, falling back to Already_exists with the given message.
func EnsureCanceled ¶
EnsureCanceled ensures the error is an *Error, falling back to Canceled with the given message.
func EnsureDataLoss ¶
EnsureDataLoss ensures the error is an *Error, falling back to Data_loss with the given message.
func EnsureDeadlineExceeded ¶
EnsureDeadlineExceeded ensures the error is an *Error, falling back to Deadline_exceeded with the given message.
func EnsureFailedPrecondition ¶
EnsureFailedPrecondition ensures the error is an *Error, falling back to Failed_precondition with the given message.
func EnsureInternal ¶
EnsureInternal ensures the error is an *Error, falling back to Internal with the given message.
func EnsureInvalidArgument ¶
EnsureInvalidArgument ensures the error is an *Error, falling back to Invalid_argument with the given message.
func EnsureNotFound ¶
EnsureNotFound ensures the error is an *Error, falling back to Not_found with the given message.
func EnsureOutOfRange ¶
EnsureOutOfRange ensures the error is an *Error, falling back to Out_of_range with the given message.
func EnsurePermissionDenied ¶
EnsurePermissionDenied ensures the error is an *Error, falling back to Permission_denied with the given message.
func EnsureResourceExhausted ¶
EnsureResourceExhausted ensures the error is an *Error, falling back to Resource_exhausted with the given message.
func EnsureUnauthenticated ¶
EnsureUnauthenticated ensures the error is an *Error, falling back to Unauthenticated with the given message.
func EnsureUnavailable ¶
EnsureUnavailable ensures the error is an *Error, falling back to Unavailable with the given message.
func EnsureUnimplemented ¶
EnsureUnimplemented ensures the error is an *Error, falling back to Unimplemented with the given message.
func EnsureUnknown ¶
EnsureUnknown ensures the error is an *Error, falling back to Unknown with the given message.
func EnsurefAborted ¶
EnsurefAborted ensures the error is an *Error, falling back to Aborted with a formatted message.
func EnsurefAlreadyExists ¶
EnsurefAlreadyExists ensures the error is an *Error, falling back to Already_exists with a formatted message.
func EnsurefCanceled ¶
EnsurefCanceled ensures the error is an *Error, falling back to Canceled with a formatted message.
func EnsurefDataLoss ¶
EnsurefDataLoss ensures the error is an *Error, falling back to Data_loss with a formatted message.
func EnsurefDeadlineExceeded ¶
EnsurefDeadlineExceeded ensures the error is an *Error, falling back to Deadline_exceeded with a formatted message.
func EnsurefFailedPrecondition ¶
EnsurefFailedPrecondition ensures the error is an *Error, falling back to Failed_precondition with a formatted message.
func EnsurefInternal ¶
EnsurefInternal ensures the error is an *Error, falling back to Internal with a formatted message.
func EnsurefInvalidArgument ¶
EnsurefInvalidArgument ensures the error is an *Error, falling back to Invalid_argument with a formatted message.
func EnsurefNotFound ¶
EnsurefNotFound ensures the error is an *Error, falling back to Not_found with a formatted message.
func EnsurefOutOfRange ¶
EnsurefOutOfRange ensures the error is an *Error, falling back to Out_of_range with a formatted message.
func EnsurefPermissionDenied ¶
EnsurefPermissionDenied ensures the error is an *Error, falling back to Permission_denied with a formatted message.
func EnsurefResourceExhausted ¶
EnsurefResourceExhausted ensures the error is an *Error, falling back to Resource_exhausted with a formatted message.
func EnsurefUnauthenticated ¶
EnsurefUnauthenticated ensures the error is an *Error, falling back to Unauthenticated with a formatted message.
func EnsurefUnavailable ¶
EnsurefUnavailable ensures the error is an *Error, falling back to Unavailable with a formatted message.
func EnsurefUnimplemented ¶
EnsurefUnimplemented ensures the error is an *Error, falling back to Unimplemented with a formatted message.
func EnsurefUnknown ¶
EnsurefUnknown ensures the error is an *Error, falling back to Unknown with a formatted message.
func New ¶
New creates a new Error with the given code and message. The message should be safe to expose to clients.
Example ¶
ExampleNew demonstrates creating a simple error with a code and message.
package main
import (
"fmt"
"github.com/bjaus/errx"
)
func main() {
err := errx.New(errx.CodeNotFound, "user not found")
fmt.Println("Code:", err.Code())
fmt.Println("Message:", err.Error())
fmt.Println("Error:", err.Error())
}
Output: Code: not_found Message: user not found Error: user not found
func NewAborted ¶
NewAborted creates a new error of type Aborted with the given message.
func NewAlreadyExists ¶
NewAlreadyExists creates a new error of type Already_exists with the given message.
func NewCanceled ¶
NewCanceled creates a new error of type Canceled with the given message.
func NewDataLoss ¶
NewDataLoss creates a new error of type Data_loss with the given message.
func NewDeadlineExceeded ¶
NewDeadlineExceeded creates a new error of type Deadline_exceeded with the given message.
func NewFailedPrecondition ¶
NewFailedPrecondition creates a new error of type Failed_precondition with the given message.
func NewInternal ¶
NewInternal creates a new error of type Internal with the given message.
func NewInvalidArgument ¶
NewInvalidArgument creates a new error of type Invalid_argument with the given message.
func NewNotFound ¶
NewNotFound creates a new error of type Not_found with the given message.
func NewOutOfRange ¶
NewOutOfRange creates a new error of type Out_of_range with the given message.
func NewPermissionDenied ¶
NewPermissionDenied creates a new error of type Permission_denied with the given message.
func NewResourceExhausted ¶
NewResourceExhausted creates a new error of type Resource_exhausted with the given message.
func NewUnauthenticated ¶
NewUnauthenticated creates a new error of type Unauthenticated with the given message.
func NewUnavailable ¶
NewUnavailable creates a new error of type Unavailable with the given message.
func NewUnimplemented ¶
NewUnimplemented creates a new error of type Unimplemented with the given message.
func NewUnknown ¶
NewUnknown creates a new error of type Unknown with the given message.
func Newf ¶
Newf creates a new Error with a formatted message. The message should be safe to expose to clients.
Example ¶
ExampleNewf demonstrates creating an error with formatted message.
package main
import (
"fmt"
"github.com/bjaus/errx"
)
func main() {
userID := 12345
err := errx.Newf(errx.CodeInvalidArgument, "invalid user ID: %d", userID)
fmt.Println(err.Error())
}
Output: invalid user ID: 12345
func NewfAborted ¶
NewfAborted creates a new error of type Aborted with a formatted message.
func NewfAlreadyExists ¶
NewfAlreadyExists creates a new error of type Already_exists with a formatted message.
func NewfCanceled ¶
NewfCanceled creates a new error of type Canceled with a formatted message.
func NewfDataLoss ¶
NewfDataLoss creates a new error of type Data_loss with a formatted message.
func NewfDeadlineExceeded ¶
NewfDeadlineExceeded creates a new error of type Deadline_exceeded with a formatted message.
func NewfFailedPrecondition ¶
NewfFailedPrecondition creates a new error of type Failed_precondition with a formatted message.
func NewfInternal ¶
NewfInternal creates a new error of type Internal with a formatted message.
func NewfInvalidArgument ¶
NewfInvalidArgument creates a new error of type Invalid_argument with a formatted message.
func NewfNotFound ¶
NewfNotFound creates a new error of type Not_found with a formatted message.
func NewfOutOfRange ¶
NewfOutOfRange creates a new error of type Out_of_range with a formatted message.
func NewfPermissionDenied ¶
NewfPermissionDenied creates a new error of type Permission_denied with a formatted message.
func NewfResourceExhausted ¶
NewfResourceExhausted creates a new error of type Resource_exhausted with a formatted message.
func NewfUnauthenticated ¶
NewfUnauthenticated creates a new error of type Unauthenticated with a formatted message.
func NewfUnavailable ¶
NewfUnavailable creates a new error of type Unavailable with a formatted message.
func NewfUnimplemented ¶
NewfUnimplemented creates a new error of type Unimplemented with a formatted message.
func NewfUnknown ¶
NewfUnknown creates a new error of type Unknown with a formatted message.
func Wrap ¶
Wrap wraps an existing error with additional context and an error code. The message should be safe to expose to clients.
Example ¶
ExampleWrap demonstrates wrapping an existing error with errx context.
package main
import (
"errors"
"fmt"
"github.com/bjaus/errx"
)
func main() {
// Simulate a database error
dbErr := errors.New("connection timeout")
// Wrap it with errx error
err := errx.Wrap(dbErr, errx.CodeUnavailable, "database unavailable")
fmt.Println(err.Error())
fmt.Println("Code:", err.Code())
fmt.Println("Original error found:", errors.Is(err, dbErr))
}
Output: database unavailable Code: unavailable Original error found: true
func WrapAborted ¶
WrapAborted wraps an existing error of type Aborted with the given message.
func WrapAlreadyExists ¶
WrapAlreadyExists wraps an existing error of type Already_exists with the given message.
func WrapCanceled ¶
WrapCanceled wraps an existing error of type Canceled with the given message.
func WrapDataLoss ¶
WrapDataLoss wraps an existing error of type Data_loss with the given message.
func WrapDeadlineExceeded ¶
WrapDeadlineExceeded wraps an existing error of type Deadline_exceeded with the given message.
func WrapFailedPrecondition ¶
WrapFailedPrecondition wraps an existing error of type Failed_precondition with the given message.
func WrapInternal ¶
WrapInternal wraps an existing error of type Internal with the given message.
func WrapInvalidArgument ¶
WrapInvalidArgument wraps an existing error of type Invalid_argument with the given message.
func WrapNotFound ¶
WrapNotFound wraps an existing error of type Not_found with the given message.
func WrapOutOfRange ¶
WrapOutOfRange wraps an existing error of type Out_of_range with the given message.
func WrapPermissionDenied ¶
WrapPermissionDenied wraps an existing error of type Permission_denied with the given message.
func WrapResourceExhausted ¶
WrapResourceExhausted wraps an existing error of type Resource_exhausted with the given message.
func WrapUnauthenticated ¶
WrapUnauthenticated wraps an existing error of type Unauthenticated with the given message.
func WrapUnavailable ¶
WrapUnavailable wraps an existing error of type Unavailable with the given message.
func WrapUnimplemented ¶
WrapUnimplemented wraps an existing error of type Unimplemented with the given message.
func WrapUnknown ¶
WrapUnknown wraps an existing error of type Unknown with the given message.
func Wrapf ¶
Wrapf wraps an existing error with a formatted message. The message should be safe to expose to clients.
func WrapfAborted ¶
WrapfAborted wraps an existing error of type Aborted with a formatted message.
func WrapfAlreadyExists ¶
WrapfAlreadyExists wraps an existing error of type Already_exists with a formatted message.
func WrapfCanceled ¶
WrapfCanceled wraps an existing error of type Canceled with a formatted message.
func WrapfDataLoss ¶
WrapfDataLoss wraps an existing error of type Data_loss with a formatted message.
func WrapfDeadlineExceeded ¶
WrapfDeadlineExceeded wraps an existing error of type Deadline_exceeded with a formatted message.
func WrapfFailedPrecondition ¶
WrapfFailedPrecondition wraps an existing error of type Failed_precondition with a formatted message.
func WrapfInternal ¶
WrapfInternal wraps an existing error of type Internal with a formatted message.
func WrapfInvalidArgument ¶
WrapfInvalidArgument wraps an existing error of type Invalid_argument with a formatted message.
func WrapfNotFound ¶
WrapfNotFound wraps an existing error of type Not_found with a formatted message.
func WrapfOutOfRange ¶
WrapfOutOfRange wraps an existing error of type Out_of_range with a formatted message.
func WrapfPermissionDenied ¶
WrapfPermissionDenied wraps an existing error of type Permission_denied with a formatted message.
func WrapfResourceExhausted ¶
WrapfResourceExhausted wraps an existing error of type Resource_exhausted with a formatted message.
func WrapfUnauthenticated ¶
WrapfUnauthenticated wraps an existing error of type Unauthenticated with a formatted message.
func WrapfUnavailable ¶
WrapfUnavailable wraps an existing error of type Unavailable with a formatted message.
func WrapfUnimplemented ¶
WrapfUnimplemented wraps an existing error of type Unimplemented with a formatted message.
func WrapfUnknown ¶
WrapfUnknown wraps an existing error of type Unknown with a formatted message.
func (*Error) DebugMessage ¶
DebugMessage returns a detailed debug message with all context. This should only be logged or shown to system maintainers, never to clients.
func (*Error) FormatStackTrace ¶
FormatStackTrace returns a human-readable stack trace.
func (*Error) Is ¶
Is supports error comparison with errors.Is. Two errors are considered equal if they have the same code.
func (*Error) IsRetryable ¶
IsRetryable returns whether the error indicates a retryable operation.
func (*Error) LogValue ¶
LogValue implements slog.LogValuer for structured logging integration. Returns a slog.GroupValue containing all error fields for debugging.
func (*Error) Source ¶
Source returns the source (service/package/component) where the error occurred.
func (*Error) StackTrace ¶
StackTrace returns the captured stack trace.
func (*Error) WithDebug ¶
WithDebug sets an internal debug message with additional implementation details. This is only shown in debug messages, never to clients.
Example ¶
ExampleError_WithDebug demonstrates separating client messages from debug details.
package main
import (
"fmt"
"github.com/bjaus/errx"
)
func main() {
err := errx.New(errx.CodeInternal, "service temporarily unavailable").
WithDebug("failed to connect to postgres.internal.company.com:5432")
fmt.Println("Client sees:", err.Error())
fmt.Println("Logs contain:", err.DebugMessage())
}
Output: Client sees: service temporarily unavailable Logs contain: [internal] service temporarily unavailable | debug=failed to connect to postgres.internal.company.com:5432
func (*Error) WithDebugf ¶
WithDebugf sets a formatted internal debug message.
func (*Error) WithDetail ¶
WithDetail adds a client-safe key-value detail to the error. These details are safe to expose to clients and are typically included in error responses.
Example ¶
ExampleError_WithDetail demonstrates adding client-safe details.
package main
import (
"fmt"
"github.com/bjaus/errx"
)
func main() {
err := errx.New(errx.CodeInvalidArgument, "validation failed").
WithDetail("field", "email").
WithDetail("reason", "invalid format")
fmt.Println("Details:", err.Details())
// These details are safe to expose to clients in the error response
}
Output: Details: map[field:email reason:invalid format]
func (*Error) WithMeta ¶
WithMeta adds a key-value pair to the error's internal metadata. This metadata is included in debug messages but NOT exposed to clients.
Example ¶
ExampleError_WithMeta demonstrates adding internal metadata for debugging.
package main
import (
"fmt"
"github.com/bjaus/errx"
)
func main() {
err := errx.New(errx.CodeInternal, "operation failed").
WithMeta("user_id", 12345).
WithMeta("operation", "update_profile").
WithMeta("retry_count", 3)
// Print metadata fields individually for deterministic output
metadata := err.Metadata()
fmt.Println("user_id:", metadata["user_id"])
fmt.Println("operation:", metadata["operation"])
fmt.Println("retry_count:", metadata["retry_count"])
// This metadata is NOT exposed to clients, only used in logs/debugging
}
Output: user_id: 12345 operation: update_profile retry_count: 3
func (*Error) WithMetaFromContext ¶
WithMetaFromContext pulls metadata stored via WithMetaContext from the context and merges it into the error's internal metadata map. Context values overwrite existing keys (last-write-wins), so call ordering determines precedence:
// Context wins — post_id will be 42:
errx.New(code, msg).WithMeta("post_id", 99).WithMetaFromContext(ctx)
// WithMeta wins — post_id will be 99:
errx.New(code, msg).WithMetaFromContext(ctx).WithMeta("post_id", 99)
func (*Error) WithRetryable ¶
WithRetryable marks the error as representing a retryable operation. This indicates that the same request can be retried and may succeed.
func (*Error) WithSource ¶
WithSource sets the source (service/package/component) where the error occurred.
Example ¶
ExampleError_WithSource demonstrates tagging errors with source (service/package/component).
package main
import (
"fmt"
"github.com/bjaus/errx"
)
func main() {
err := errx.New(errx.CodeUnavailable, "service unavailable").
WithSource("payment-service")
fmt.Println("Source:", err.Source())
// Useful for identifying which service/component generated the error
}
Output: Source: payment-service
func (*Error) WithTags ¶
WithTags adds one or more tags to categorize the error.
Example ¶
ExampleError_WithTags demonstrates categorizing errors with tags.
package main
import (
"fmt"
"github.com/bjaus/errx"
)
func main() {
err := errx.New(errx.CodeInternal, "operation failed").
WithTags("database", "postgres").
WithTags("critical")
fmt.Println("Tags:", err.Tags())
// Tags can be used for filtering, alerting, or categorizing errors
}
Output: Tags: [database postgres critical]