https://github.com/m-mizutani/goerr
More contextual error handling in Go
https://github.com/m-mizutani/goerr
error-handling go
Last synced: 5 months ago
JSON representation
More contextual error handling in Go
- Host: GitHub
- URL: https://github.com/m-mizutani/goerr
- Owner: m-mizutani
- License: bsd-2-clause
- Created: 2021-04-09T23:49:48.000Z (about 5 years ago)
- Default Branch: main
- Last Pushed: 2025-09-19T00:38:10.000Z (10 months ago)
- Last Synced: 2025-12-22T19:45:38.617Z (6 months ago)
- Topics: error-handling, go
- Language: Go
- Homepage:
- Size: 166 KB
- Stars: 42
- Watchers: 1
- Forks: 4
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# goerr [](https://github.com/m-mizutani/goerr/actions/workflows/test.yml) [](https://github.com/m-mizutani/goerr/actions/workflows/gosec.yml) [](https://github.com/m-mizutani/goerr/actions/workflows/trivy.yml) [](https://pkg.go.dev/github.com/m-mizutani/goerr/v2)
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
```sh
go get github.com/m-mizutani/goerr/v2
```
## Quick Start
```go
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:
```go
// 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`:
```go
// 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:
```go
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:
```go
// 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:
```go
// 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`:
```go
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:
```go
// 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:
```go
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:
```go
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:
```go
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:
```go
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](./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](./docs/migration.md) for migrating from:
- `github.com/pkg/errors`
- Standard library `errors` package
- goerr v1 to v2
## License
The 2-Clause BSD License. See [LICENSE](LICENSE) for more detail.