cause

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Nov 20, 2025 License: BSD-3-Clause Imports: 8 Imported by: 2

README

Error Cause Package

A comprehensive Go error handling package that provides structured error management with support for error codes, attributes, validation, stack traces, and structured logging.

Features

  • Structured Errors: Rich error types with codes, names, messages, and additional context
  • Error Chaining: Support for wrapping and unwrapping errors
  • Validation Framework: Fluent validation API for complex data structures
  • Structured Logging: Integration with Go's slog package
  • Stack Traces: Optional stack trace capture for debugging
  • Type Safety: Strong typing with generic support for validation

Installation

go get github.com/alextanhongpin/errors/cause

Quick Start

Basic Error Creation
package main

import (
    "fmt"
    "github.com/alextanhongpin/errors/cause"
    "github.com/alextanhongpin/errors/codes"
)

// Define error constants
var (
    ErrUserNotFound = cause.New(codes.NotFound, "UserNotFound", "User not found")
    ErrInvalidEmail = cause.New(codes.Invalid, "InvalidEmail", "Invalid email format")
)

func main() {
    err := ErrUserNotFound.WithDetails(map[string]any{
        "user_id": "123",
        "action":  "fetch",
    })
    
    fmt.Println(err.Error()) // Output: User not found
    fmt.Println(err.Code)    // Output: not_found
}
Error with Stack Trace
func riskyOperation() error {
    return ErrUserNotFound.WithStack()
}
Error Chaining
func fetchUser(id string) error {
    if err := validateUserID(id); err != nil {
        return ErrInvalidEmail.Wrap(err)
    }
    // ... fetch logic
    return nil
}

Validation Framework

The package includes a powerful validation framework for validating complex data structures:

Simple Validation
type User struct {
    Name string
    Age  int
}

func (u *User) Validate() error {
    return cause.Map{
        "name": cause.Required(u.Name),
        "age":  cause.Optional(u.Age).When(u.Age < 0, "must be positive"),
    }.Err()
}
Nested Validation
type Address struct {
    Street string
    City   string
}

func (a *Address) Validate() error {
    return cause.Map{
        "street": cause.Required(a.Street),
        "city":   cause.Required(a.City),
    }.Err()
}

type User struct {
    Name    string
    Address *Address
}

func (u *User) Validate() error {
    return cause.Map{
        "name":    cause.Required(u.Name),
        "address": cause.Required(u.Address),
    }.Err()
}
Slice Validation
func validateUsers(users []User) error {
    return cause.SliceFunc(users, func(u User) error {
        return u.Validate()
    }).Validate()
}

Structured Logging

The package integrates seamlessly with Go's slog package:

import "log/slog"

func main() {
    logger := slog.Default()
    
    err := cause.New(codes.Invalid, "ValidationError", "Invalid input").
        WithAttrs(slog.String("field", "email")).
        WithDetails(map[string]any{
            "input": "invalid-email",
            "rule":  "email_format",
        })
    
    logger.Error("Validation failed", "error", err)
}

Advanced Usage

Custom Validation Conditions
func (u *User) Validate() error {
    return cause.Map{
        "email": cause.Required(u.Email).
            When(!isValidEmail(u.Email), "invalid email format").
            When(isDomainBlocked(u.Email), "email domain not allowed"),
        "age": cause.Optional(u.Age).
            When(u.Age < 13, "under minimum age").
            When(u.Age > 120, "age not realistic"),
    }.Err()
}
Error Type Checking
func handleError(err error) {
    var causeErr *cause.Error
    if errors.As(err, &causeErr) {
        switch causeErr.Code {
        case codes.NotFound:
            // Handle not found
        case codes.Invalid:
            // Handle validation errors
        }
    }
}
Validation Error Handling
func processValidationError(err error) {
    if validationErr, ok := err.(interface{ Map() map[string]any }); ok {
        fieldErrors := validationErr.Map()
        for field, fieldErr := range fieldErrors {
            fmt.Printf("Field %s: %v\n", field, fieldErr)
        }
    }
}

API Reference

Error Type

The main Error type provides:

  • Code: Error classification using codes package
  • Name: Unique error type identifier
  • Message: Human-readable error description
  • Attrs: Structured logging attributes
  • Details: Additional context data
  • Cause: Wrapped underlying error
  • Stack: Optional stack trace
Methods
  • New(code, name, message, ...args) *Error: Create new error
  • WithStack() *Error: Add stack trace
  • WithDetails(map[string]any) *Error: Add context details
  • WithAttrs(...slog.Attr) *Error: Add logging attributes
  • Wrap(error) *Error: Wrap another error
  • Clone() *Error: Create deep copy
Validation Functions
  • Required(val) *Builder: Validate required field
  • Optional(val) *Builder: Validate optional field
  • Map{}.Err(): Validate multiple fields
  • SliceFunc(slice, func) sliceValidator: Validate slice elements
Builder Methods
  • When(condition, message) *Builder: Conditional validation
  • Validate() error: Execute validation chain

Error Codes

This package works with the companion codes package that provides standard error classifications:

  • codes.OK: Success
  • codes.Invalid: Invalid input
  • codes.NotFound: Resource not found
  • codes.AlreadyExists: Resource already exists
  • codes.PermissionDenied: Access denied
  • codes.Internal: Internal server error
  • And more...

Best Practices

  1. Define Error Constants: Create package-level error constants for reusable errors
  2. Use Meaningful Names: Choose descriptive error names and codes
  3. Add Context: Use WithDetails() to provide debugging context
  4. Implement Validation: Use the validation framework for input validation
  5. Chain Errors: Use Wrap() to preserve error context
  6. Structured Logging: Leverage slog integration for better observability

Examples

See the examples_*_test.go files for comprehensive usage examples including:

  • Basic error creation and handling
  • Validation patterns
  • Logging integration
  • Error chaining
  • Complex validation scenarios

Real-World Examples

The package includes comprehensive real-world examples demonstrating validation in different domains:

Healthcare System (examples_healthcare_test.go)
  • Patient record management with medical validations
  • Healthcare-specific field validation (blood types, medical IDs, vital signs)
  • Complex nested structures with emergency contacts and insurance information
Educational Institution (examples_education_test.go)
  • Student enrollment system with academic profile validation
  • Course registration with prerequisites and scheduling
  • Transcript management and GPA calculations
IoT Device Management (examples_iot_test.go)
  • Device configuration validation with network settings
  • Sensor calibration and threshold management
  • Security configuration and API key management
  • Power management and location tracking
Business Applications (examples_business_test.go, examples_api_test.go)
  • Financial transaction validation
  • API request/response validation
  • E-commerce product and order management
  • User registration and authentication

Each example demonstrates:

  • Complex nested validation scenarios
  • Domain-specific validation rules
  • Real-world field constraints and business logic
  • Best practices for structuring validation code

Contributing

Contributions are welcome! Please read our contributing guidelines and submit pull requests for any improvements.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Package cause provides structured error handling with support for error codes, attributes, details, stack traces, and validation. It extends Go's standard error interface with additional context and structured logging capabilities.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrAborted            = New(codes.Aborted, "ABORTED", "The operation was aborted")
	ErrBadRequest         = New(codes.BadRequest, "BAD_REQUEST", "The request is invalid")
	ErrCanceled           = New(codes.Canceled, "CANCELED", "The operation was canceled")
	ErrConflict           = New(codes.Conflict, "CONFLICT", "The request could not be completed due to a conflict with the current state of the target resource")
	ErrDataLoss           = New(codes.DataLoss, "DATA_LOSS", "Unrecoverable data loss or corruption")
	ErrDeadlineExceeded   = New(codes.DeadlineExceeded, "DEADLINE_EXCEEDED", "The deadline expired before the operation could complete")
	ErrExists             = New(codes.Exists, "EXISTS", "The resource that a client tried to create already exists")
	ErrForbidden          = New(codes.Forbidden, "FORBIDDEN", "The caller does not have permission to execute the specified operation")
	ErrInternal           = New(codes.Internal, "INTERNAL", "Internal server error")
	ErrNotFound           = New(codes.NotFound, "NOT_FOUND", "The specified resource was not found")
	ErrNotImplemented     = New(codes.NotImplemented, "NOT_IMPLEMENTED", "The operation is not implemented or not supported")
	ErrOutOfRange         = New(codes.OutOfRange, "OUT_OF_RANGE", "The operation was attempted past the valid range")
	ErrPreconditionFailed = New(codes.PreconditionFailed, "PRECONDITION_FAILED", "The operation was rejected because the system is not in a state required for the operation's execution")
	ErrTooManyRequests    = New(codes.TooManyRequests, "TOO_MANY_REQUESTS", "The caller has sent too many requests in a given amount of time")
	ErrUnauthorized       = New(codes.Unauthorized, "UNAUTHORIZED", "The request does not have valid authentication credentials for the operation")
	ErrUnavailable        = New(codes.Unavailable, "UNAVAILABLE", "The service is currently unavailable")
	ErrUnknown            = New(codes.Unknown, "UNKNOWN", "An unknown error occurred")
)

Functions

This section is empty.

Types

type Error

type Error struct {
	// Attrs contains structured logging attributes
	Attrs []slog.Attr
	// Cause is the underlying wrapped error
	Cause error
	// Code represents the error classification
	Code codes.Code
	// Details contains additional context as key-value pairs
	Details map[string]any
	// Message is the human-readable error message
	Message string
	// Name is a unique identifier for this error type
	Name string
	// Stack contains the stack trace when the error was created
	Stack string
}

Error represents a structured error with additional context including error codes, attributes, details, stack traces, and nested causes. It implements the standard error interface and provides enhanced logging capabilities.

Example (Log_attr)
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log/slog"
	"time"

	"github.com/alextanhongpin/errors/cause"
	"github.com/alextanhongpin/errors/codes"
)

func main() {
	var err error = cause.New(codes.BadRequest, "BadRequest", "email=%s is invalid", "[email protected]").
		WithAttrs(slog.String("email", "[email protected]"))

	replacer := func(groups []string, a slog.Attr) slog.Attr {
		if a.Key == "time" {
			a.Value = slog.TimeValue(time.Date(2025, 6, 7, 0, 52, 24, 115438000, time.UTC))
		}
		return a
	}

	b := new(bytes.Buffer)
	logger := slog.New(slog.NewJSONHandler(b, &slog.HandlerOptions{AddSource: true, ReplaceAttr: replacer}))
	logger.Error("payment failed", slog.Any("error", err))

	data := b.Bytes()
	b.Reset()
	if err := json.Indent(b, data, "", "  "); err != nil {
		panic(err)
	}

	fmt.Println(b.String())

}
Output:

{
  "time": "2025-06-07T00:52:24.115438Z",
  "level": "ERROR",
  "source": {
    "function": "github.com/alextanhongpin/errors/cause_test.ExampleError_log_attr",
    "file": "/Users/alextanhongpin/Documents/go/errors/cause/examples_log_attr_test.go",
    "line": 27
  },
  "msg": "payment failed",
  "error": {
    "message": "[email protected] is invalid",
    "code": "bad_request",
    "name": "BadRequest",
    "data": {
      "email": "[email protected]"
    }
  }
}
Example (Marshal)
package main

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"log"

	"github.com/alextanhongpin/errors/cause"
	"github.com/alextanhongpin/errors/codes"
)

var ErrUnknown = cause.New(codes.Unknown, "Unknown error", "This needs to be fixed")

func main() {
	var err error = ErrUnknown.WithCause(sql.ErrNoRows)
	b, err := json.Marshal(err)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(b))

	var causeErr *cause.Error
	if err := json.Unmarshal(b, &causeErr); err != nil {
		log.Fatal(err)
	}

	fmt.Println("is sql.ErrNoRows?:", errors.Is(causeErr, sql.ErrNoRows))
	fmt.Println("is ErrUnknown?:", errors.Is(causeErr, ErrUnknown))

}
Output:

{"cause":{"code":17,"message":"sql: no rows in result set","name":"Unknown"},"code":17,"message":"This needs to be fixed","name":"Unknown error"}
is sql.ErrNoRows?: true
is ErrUnknown?: true
Example (Marshal_nested)
package main

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"log"

	"github.com/alextanhongpin/errors/cause"
	"github.com/alextanhongpin/errors/codes"
)

var (
	ErrUnknown = cause.New(codes.Unknown, "Unknown error", "This needs to be fixed")
	ErrNested  = cause.New(codes.Unknown, "Nested error", "One level of nesting")
)

func main() {
	var err error = ErrUnknown.WithCause(ErrNested.WithCause(sql.ErrNoRows))
	b, err := json.Marshal(err)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(b))

	var causeErr *cause.Error
	if err := json.Unmarshal(b, &causeErr); err != nil {
		log.Fatal(err)
	}

	fmt.Println("is sql.ErrNoRows?:", errors.Is(causeErr, sql.ErrNoRows))
	fmt.Println("is ErrUnknown?:", errors.Is(causeErr, ErrUnknown))
	fmt.Println("is ErrNested?:", errors.Is(causeErr, ErrNested))

}
Output:

{"cause":{"cause":{"code":17,"message":"sql: no rows in result set","name":"Unknown"},"code":17,"message":"One level of nesting","name":"Nested error"},"code":17,"message":"This needs to be fixed","name":"Unknown error"}
is sql.ErrNoRows?: true
is ErrUnknown?: true
is ErrNested?: true

func New

func New(code codes.Code, name, message string, args ...any) *Error

New creates a new Error with the specified code, name, and message. Additional arguments can include slog.Attr for structured logging attributes. The message supports fmt.Sprintf formatting with the provided args.

Example:

err := New(codes.NotFound, "UserNotFound", "User %s not found", userID)
Example
package main

import (
	"errors"
	"fmt"

	"github.com/alextanhongpin/errors/cause"
	"github.com/alextanhongpin/errors/codes"
)

var ErrUserNotFound = cause.New(codes.NotFound, "UserNotFoundError", "User not found")

func main() {
	var err error = ErrUserNotFound
	fmt.Println("err:", err)
	fmt.Println("is:", errors.Is(err, ErrUserNotFound))

	var causeErr *cause.Error
	if errors.As(err, &causeErr) {
		fmt.Println("code:", causeErr.Code)
		fmt.Println("details:", causeErr.Details)
		fmt.Println("message:", causeErr.Message)
		fmt.Println("name:", causeErr.Name)
	}

}
Output:

err: User not found
is: true
code: not_found
details: map[]
message: User not found
name: UserNotFoundError

func (*Error) Clone

func (e *Error) Clone() *Error

Clone creates a deep copy of the error, allowing safe modification without affecting the original error.

func (*Error) Error

func (e *Error) Error() string

Error returns the error message, implementing the standard error interface.

func (*Error) Is

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

Is reports whether this error matches the target error. Two errors match if they have the same Code and Name.

func (Error) LogValue

func (e Error) LogValue() slog.Value

LogValue implements slog.LogValuer for structured logging. It returns a grouped slog.Value containing all error context including message, code, name, attributes, details, and cause.

Example
package main

import (
	"bytes"
	"database/sql"
	"encoding/json"
	"fmt"
	"log/slog"
	"time"

	"github.com/alextanhongpin/errors/cause"
	"github.com/alextanhongpin/errors/codes"
)

var ErrDuplicateRow = cause.New(codes.Exists, "DuplicateRowError", "Duplicate row")
var ErrPaymentFailed = cause.New(codes.Conflict, "PaymentFailedError", "Duplicate payment attempt")

func main() {
	var err error = ErrPaymentFailed.WithDetails(map[string]any{
		"order_id": "12345",
	}).WithCause(ErrDuplicateRow.WithCause(sql.ErrNoRows))
	fmt.Println(err)

	replacer := func(groups []string, a slog.Attr) slog.Attr {
		if a.Key == "time" {
			a.Value = slog.TimeValue(time.Date(2025, 6, 7, 0, 52, 24, 115438000, time.UTC))
		}
		return a
	}

	b := new(bytes.Buffer)
	logger := slog.New(slog.NewJSONHandler(b, &slog.HandlerOptions{AddSource: true, ReplaceAttr: replacer}))
	logger.Error("payment failed", slog.Any("error", err))

	data := b.Bytes()
	b.Reset()
	if err := json.Indent(b, data, "", "  "); err != nil {
		panic(err)
	}
	fmt.Println(b.String())

}
Output:

Duplicate payment attempt
Caused by: Duplicate row
Caused by: sql: no rows in result set
{
  "time": "2025-06-07T00:52:24.115438Z",
  "level": "ERROR",
  "source": {
    "function": "github.com/alextanhongpin/errors/cause_test.ExampleError_LogValue",
    "file": "/Users/alextanhongpin/Documents/go/errors/cause/examples_log_value_test.go",
    "line": 33
  },
  "msg": "payment failed",
  "error": {
    "message": "Duplicate payment attempt",
    "code": "conflict",
    "name": "PaymentFailedError",
    "cause": {
      "message": "Duplicate row",
      "code": "exists",
      "name": "DuplicateRowError",
      "cause": "sql: no rows in result set"
    },
    "details": {
      "order_id": "12345"
    }
  }
}

func (*Error) MarshalJSON added in v0.3.2

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

func (*Error) UnmarshalJSON added in v0.3.2

func (e *Error) UnmarshalJSON(b []byte) error

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying cause error, supporting Go's error unwrapping.

func (*Error) WithAttrs

func (e *Error) WithAttrs(attrs ...slog.Attr) *Error

WithAttrs returns a new error with additional structured logging attributes.

func (*Error) WithCause added in v0.3.0

func (e *Error) WithCause(cause error) *Error

WithCause returns a new error with the specified cause error.

Example
package main

import (
	"database/sql"
	"errors"
	"fmt"

	"github.com/alextanhongpin/errors/cause"
	"github.com/alextanhongpin/errors/codes"
)

var ErrStorage = cause.New(codes.Internal, "StorageError", "Storage error")

func main() {
	var err error = ErrStorage.WithCause(sql.ErrNoRows)
	fmt.Println("is sql.ErrNoRows?:", errors.Is(err, sql.ErrNoRows))
	fmt.Println("is ErrStorage?:", errors.Is(err, ErrStorage))

	var causeErr *cause.Error
	if errors.As(err, &causeErr) {
		fmt.Println("cause:", causeErr.Unwrap())
	}
	fmt.Println(ErrStorage.Unwrap())

}
Output:

is sql.ErrNoRows?: true
is ErrStorage?: true
cause: sql: no rows in result set
<nil>

func (*Error) WithDetails

func (e *Error) WithDetails(details map[string]any) *Error

WithDetails returns a new error with additional details merged in. Existing details are preserved, new details override existing keys.

func (*Error) WithMessage added in v0.3.0

func (e *Error) WithMessage(message string, args ...any) *Error

WithMessage returns a new error with the specified message formatted with args.

func (*Error) WithStack

func (e *Error) WithStack() *Error

WithStack returns a new error with the current stack trace captured.

Example
package main

import (
	"database/sql"
	"fmt"
	"log/slog"
	"os"

	"github.com/alextanhongpin/errors/cause"
	"github.com/alextanhongpin/errors/codes"
)

func main() {
	err := bar()
	fmt.Println(err)
	fmt.Println()

	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		AddSource: true,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey && len(groups) == 0 {
				return slog.Attr{}
			}
			return a
		}}))
	logger.Error("An error occurred", "error", err)

}

func foo() error {
	return cause.New(codes.Internal, "StackError", "An error with stack trace").
		WithCause(sql.ErrNoRows).
		WithStack()
}

func bar() error {
	return cause.New(codes.Internal, "BarError", "An error in bar").
		WithStack().
		WithCause(foo())
}
Output:

An error in bar
	at /Users/alextanhongpin/Documents/go/errors/cause/examples_stack_trace_test.go.46
Caused by: An error with stack trace
	at /Users/alextanhongpin/Documents/go/errors/cause/examples_stack_trace_test.go.41
Caused by: sql: no rows in result set

{"level":"ERROR","source":{"function":"github.com/alextanhongpin/errors/cause_test.ExampleError_WithStack","file":"/Users/alextanhongpin/Documents/go/errors/cause/examples_stack_trace_test.go","line":26},"msg":"An error occurred","error":{"message":"An error in bar","code":"internal","name":"BarError","cause":{"message":"An error with stack trace","code":"internal","name":"StackError","cause":"sql: no rows in result set"}}}

Jump to

Keyboard shortcuts

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