Skip to content

Latest commit

 

History

History
371 lines (285 loc) · 9.7 KB

File metadata and controls

371 lines (285 loc) · 9.7 KB

goerr test gosec package scan Go Reference

Enhanced error handling for Go with stack traces, contextual values, and structured logging

Overview

goerr is a powerful error handling library for Go that enhances errors with rich contextual information. It provides stack traces, contextual variables, error categorization, and seamless integration with structured logging - all while maintaining full compatibility with Go's standard error handling patterns.

Key Features

  • Stack Traces: Automatic capture with github.com/pkg/errors compatibility
  • Contextual Data: Attach key-value pairs and tags to errors
  • Type Safety: Compile-time type checking for error context
  • Multiple Errors: Aggregate errors with goerr.Errors
  • Structured Logging: Native slog integration

Installation

go get github.com/m-mizutani/goerr/v2

Quick Start

package main

import (
    "log"
    "github.com/m-mizutani/goerr/v2"
)

func main() {
    if err := processFile("data.txt"); err != nil {
        // Print error with stack trace
        log.Fatalf("%+v", err)
    }
}

func processFile(filename string) error {
    _, err := readFile(filename)
    if err != nil {
        return goerr.Wrap(err, "failed to process file",
            goerr.Value("filename", filename))
    }
    return nil
}

func readFile(filename string) error {
    // Simulate error
    return goerr.New("file not found")
}

Core Features

Error Creation and Wrapping

Create new errors or wrap existing ones with additional context:

// Create a new error
err := goerr.New("validation failed")

// Wrap an existing error
if err := someFunc(); err != nil {
    return goerr.Wrap(err, "operation failed")
}

// Add contextual information without changing the original error
err = goerr.With(err,
    goerr.Value("user_id", userID),
    goerr.Value("timestamp", time.Now()))

// With preserves stacktrace for goerr.Error, wraps standard errors
originalErr := goerr.New("original error")
enhanced := goerr.With(originalErr, goerr.Value("context", "added"))
// enhanced has same stacktrace as originalErr, originalErr unchanged

// Key precedence: later values override earlier ones
err := goerr.New("error", goerr.Value("key", "first"))
enhanced := goerr.With(err,
    goerr.Value("key", "second"),  // Overrides "first"
    goerr.Value("key", "final"))   // Overrides "second"
// enhanced.Values()["key"] == "final"

// Extract goerr.Error from any error
if goErr := goerr.Unwrap(err); goErr != nil {
    values := goErr.Values() // Get all contextual values
}

Multiple Error Handling

Aggregate multiple errors with goerr.Errors:

// Collect errors during processing
var errs *goerr.Errors
for _, item := range items {
    if err := processItem(item); err != nil {
        errs = goerr.Append(errs, err)  // nil-safe
    }
}

// Return only if errors occurred
return errs.ErrorOrNil()  // nil if no errors

// Join errors directly
combined := goerr.Join(err1, err2, err3)

// All errors displayed together
fmt.Printf("%v", combined)
// Output: error1\nerror2\nerror3

// Works with standard library
if errors.Is(combined, err1) { /* true */ }

Contextual Data

String-based Values

Attach arbitrary key-value pairs to errors:

func validateUser(userID string, age int) error {
    if age < 18 {
        return goerr.New("user too young",
            goerr.V("user_id", userID),  // V is alias for Value
            goerr.V("age", age),
            goerr.V("required_age", 18))
    }
    return nil
}

// Extract values from error
if err := validateUser("user123", 16); err != nil {
    if goErr := goerr.Unwrap(err); goErr != nil {
        for key, value := range goErr.Values() {
            log.Printf("%s: %v", key, value)
        }
    }
}

Type-safe Values

Use compile-time type checking for error context:

// Define typed keys (typically at package level)
var (
    UserIDKey    = goerr.NewTypedKey[string]("user_id")
    RequestIDKey = goerr.NewTypedKey[int64]("request_id")
    ConfigKey    = goerr.NewTypedKey[*Config]("config")
)

// Use typed values - compile-time type checking
err := goerr.New("validation failed",
    goerr.TV(UserIDKey, "user123"),      // Must be string
    goerr.TV(RequestIDKey, int64(42)),   // Must be int64
    goerr.TV(ConfigKey, currentConfig))  // Must be *Config

// Retrieve typed values - no type assertion needed
if userID, ok := goerr.GetTypedValue(err, UserIDKey); ok {
    // userID is string type, guaranteed
    fmt.Printf("User: %s\n", userID)
}

Error Tags

Categorize errors for different handling strategies:

// Define tags
var (
    ErrTagNotFound   = goerr.NewTag("not_found")
    ErrTagValidation = goerr.NewTag("validation")
    ErrTagExternal   = goerr.NewTag("external")
)

// Tag errors
if user == nil {
    return goerr.New("user not found",
        goerr.T(ErrTagNotFound))  // T is alias for Tag
}

// Handle errors based on tags
if goerr.HasTag(err, ErrTagNotFound) {
    w.WriteHeader(http.StatusNotFound)
} else if goerr.HasTag(err, ErrTagValidation) {
    w.WriteHeader(http.StatusBadRequest)
} else {
    w.WriteHeader(http.StatusInternalServerError)
}

Stack Traces

Stack traces are automatically captured and compatible with github.com/pkg/errors:

func doWork() error {
    return goerr.New("something went wrong")
}

func main() {
    if err := doWork(); err != nil {
        // Print with stack trace using %+v
        log.Printf("%+v", err)
        
        // Extract stack programmatically
        if goErr := goerr.Unwrap(err); goErr != nil {
            for _, frame := range goErr.Stacks() {
                log.Printf("  at %s:%d in %s", 
                    frame.File, frame.Line, frame.Func)
            }
        }
    }
}

// Remove current frame from stack (useful for helper functions)
func helperFunc() error {
    return goerr.New("error from helper").Unstack()
}

Advanced Features

Enhancing Errors with Context

The With function adds contextual information to errors without modifying the original:

// For goerr.Error: preserves existing stacktrace
originalErr := goerr.New("database connection failed")
enhanced := goerr.With(originalErr,
    goerr.Value("host", "db.example.com"),
    goerr.Value("port", 5432),
    goerr.Tag(ErrTagExternal))

// originalErr remains unchanged, enhanced has same stacktrace
fmt.Printf("Original unchanged: %v\n", originalErr.Values()) // empty
fmt.Printf("Enhanced: %v\n", enhanced.Values())              // has host, port

// For standard errors: wraps with new stacktrace
stdErr := errors.New("file not found")
enhanced2 := goerr.With(stdErr, goerr.Value("path", "/tmp/file.txt"))
// enhanced2 wraps stdErr with new stacktrace and context

Error Identification

Use IDs for flexible error comparison:

var (
    ErrInvalidInput = goerr.New("invalid input", goerr.ID("ERR_INVALID_INPUT"))
    ErrTimeout      = goerr.New("operation timeout", goerr.ID("ERR_TIMEOUT"))
)

func process() error {
    return goerr.Wrap(ErrInvalidInput, "validation failed",
        goerr.Value("field", "email"))
}

// Check error identity
if err := process(); err != nil {
    if errors.Is(err, ErrInvalidInput) {
        // Matches by ID, not pointer
        handleValidationError(err)
    }
}

Builder Pattern

Create multiple errors with shared context:

type Service struct {
    userID string
    reqID  string
}

func (s *Service) process() error {
    // Create builder with common context
    eb := goerr.NewBuilder(
        goerr.Value("user_id", s.userID),
        goerr.Value("request_id", s.reqID))
    
    // Use builder for multiple errors
    if err := s.validate(); err != nil {
        return eb.Wrap(err, "validation failed")
    }
    
    if err := s.save(); err != nil {
        return eb.Wrap(err, "save failed")
    }
    
    return nil
}

Structured Logging

Native integration with Go's slog package:

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

err := goerr.New("database error",
    goerr.Value("table", "users"),
    goerr.Value("operation", "insert"))

// Error implements slog.LogValuer
logger.Error("operation failed", slog.Any("error", err))

// Output (formatted):
// {
//   "level": "ERROR",
//   "msg": "operation failed",
//   "error": {
//     "message": "database error",
//     "values": {"table": "users", "operation": "insert"},
//     "stacktrace": [...]
//   }
// }

JSON Serialization

Export full error details as JSON:

err := goerr.New("validation error",
    goerr.Value("field", "email"),
    goerr.Tag(ValidationTag))

// Get JSON-serializable struct
printable := goerr.Unwrap(err).Printable()

// Or marshal directly
jsonData, _ := json.Marshal(err)

// Output includes message, stack trace, values, tags, and cause chain

Examples

See the examples directory for complete working examples:

  • Stack trace handling
  • Contextual variables
  • Multiple error aggregation
  • HTTP error responses
  • Sentry integration
  • Structured logging with slog
  • And more...

Migration Guide

See Migration Guide for migrating from:

  • github.com/pkg/errors
  • Standard library errors package
  • goerr v1 to v2

License

The 2-Clause BSD License. See LICENSE for more detail.