Enhanced error handling for Go with stack traces, contextual values, and structured logging
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.
- Stack Traces: Automatic capture with
github.com/pkg/errorscompatibility - 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
slogintegration
go get github.com/m-mizutani/goerr/v2package 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")
}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
}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 */ }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 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()
}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 contextUse 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)
}
}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
}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": [...]
// }
// }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 chainSee 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...
See Migration Guide for migrating from:
github.com/pkg/errors- Standard library
errorspackage - goerr v1 to v2
The 2-Clause BSD License. See LICENSE for more detail.