Errors package for structured logging with native log/slog integration. Adds stack trace without a pain
(no confuse with Wrap/WithMessage methods).
⚠️ Breaking Changes in v0.5.0
Version 0.5.0 replaces the custom field system with nativelog/slogintegration.
If you're migrating from v0.4.1, please read the Migration Guide.
This package is based on well known github.com/pkg/errors. Key differences and features:
errors.New()is an alias to standard library and (it does not add a stack trace) and should be used to create sentinel package-level errors;- minimalistic API: few methods to wrap an error:
errors.Errorf(),errors.Wrap(); - adds stack trace idempotently (only once in a chain);
errors.As()method is based on typed parameters (aka generics);- options to skip caller in a stack trace and to add error attributes for structured logging;
- native integration with Go's
log/slog- error attributes useslog.Attr; - supports grouped attributes via
slog.Group; - implements
slog.LogValuerfor seamless slog integration; - package errors can be easily marshaled into JSON with all fields in a chain.
errors.IsOfType[T any](err error)to test for error types.errors.Attrs(err error) []slog.Attrto extract all attributes from error chain.errors.Log(ctx, logger, err)anderrors.LogLevel(ctx, logger, level, err)for logging with slog.
Run the following command to install the package
go get -u github.com/muonsoft/errors
Requires Go 1.21+ for log/slog support.
Migrating from v0.4.1? See the Migration Guide for detailed instructions.
errors.New() is an alias to the standard errors.New() function. Use it only for sentinel package-level errors.
This function would not add a stack trace.
var ErrNotFound = errors.New("not found")
var errInternalError = errors.New("internal error")
// To initiate a sentinel error with a stack trace it is recommended to use a
// constructor function and wrap the error with errors.Wrap().
// Use errors.SkipCaller() option to remove constructor function from a stack trace.
func NewNotFoundError() error {
return errors.Wrap(ErrNotFound, errors.SkipCaller())
}errors.Errorf() is an equivalent to standard fmt.Errorf(). It formats according to a format specifier
and returns the string as a value that satisfies error. You can wrap an error using %w modifier.
errors.Errorf() also records the stack trace at the point it was called. If the wrapped error
contains a stack trace then a new one will not be added to a chain.
You can pass options to set structured attributes or to skip a caller in a stack trace.
Both helper functions like errors.String() and slog.Attr directly are supported.
Options/attributes must be specified after formatting arguments.
row := repository.db.QueryRow(ctx, findSQL, id)
var product Product
err := row.Scan(&product.ID, &product.Name)
if err != nil {
// Option 1: Use helper functions
return nil, errors.Errorf(
"%w: %v", errSQLError, err.Error(),
errors.String("sql", findSQL),
errors.Int("productID", id),
)
// Option 2: Use slog.Attr directly (more idiomatic)
return nil, errors.Errorf(
"%w: %v", errSQLError, err.Error(),
slog.String("sql", findSQL),
slog.Int("productID", id),
)
}errors.Wrap() returns an error annotating err with a stack trace at the point errors.Wrap() is called.
If the wrapped error contains a stack trace then a new one will not be added to a chain.
If err is nil, Wrap returns nil.
You can pass options to set structured attributes or to skip a caller in a stack trace.
Both helper functions like errors.String() and slog.Attr directly are supported.
data, err := service.Handle(ctx, userID, message)
if err != nil {
// Option 1: Use helper functions
return nil, errors.Wrap(
err,
errors.Int("userID", userID),
errors.String("userMessage", message),
)
// Option 2: Use slog.Attr directly (recommended)
return nil, errors.Wrap(
err,
slog.Int("userID", userID),
slog.String("userMessage", message),
)
// Option 3: Mix both styles
return nil, errors.Wrap(
err,
errors.SkipCaller(), // Option for stack trace
slog.String("userMessage", message), // slog.Attr for logging
)
}The package has native slog integration - you can pass slog.Attr directly to Wrap() and Errorf():
// Use slog attributes directly (recommended)
err := errors.Wrap(
dbErr,
slog.String("table", "users"),
slog.Int("id", 123),
slog.Duration("query_time", 50*time.Millisecond),
)
// Or use helper functions (equivalent)
err := errors.Wrap(
dbErr,
errors.String("table", "users"),
errors.Int("id", 123),
errors.Duration("query_time", 50*time.Millisecond),
)
// All slog types are supported
err := errors.Wrap(
err,
slog.Bool("cached", false),
slog.Int64("timestamp", time.Now().Unix()),
slog.Uint64("bytes_written", uint64(1024*1024*500)),
slog.Float64("cpu_usage", 0.85),
slog.Any("metadata", map[string]interface{}{
"version": "v1.2.3",
"region": "us-west-1",
}),
)Organize related attributes using slog.Group directly:
err := errors.Wrap(
dbErr,
slog.Group("request",
slog.String("method", "POST"),
slog.String("path", "/api/users"),
slog.Int("status", 500),
),
slog.Group("database",
slog.String("query", "INSERT INTO users..."),
slog.Duration("duration", 150*time.Millisecond),
),
)
// Or use the errors.Group helper
err := errors.Wrap(
dbErr,
errors.Group("request",
slog.String("method", "POST"),
slog.String("path", "/api/users"),
),
)You can use formatting with %+v modifier to print errors with message, attributes and stack trace.
Grouped attributes are displayed using dot notation.
Example
func main() {
err := errors.Errorf(
"sql error: %w", sql.ErrNoRows,
errors.String("sql", "SELECT id, name FROM product WHERE id = ?"),
errors.Int("productID", 123),
)
err = errors.Errorf(
"find product: %w", err,
errors.Group("request",
slog.String("id", "24874020-cab7-4ef3-bac5-76858832f8b0"),
slog.String("method", "GET"),
),
)
fmt.Printf("%+v", err)
}Output
find product: sql error: sql: no rows in result set
request.id: 24874020-cab7-4ef3-bac5-76858832f8b0
request.method: GET
sql: SELECT id, name FROM product WHERE id = ?
productID: 123
main.main
/home/user/project/main.go:11
runtime.main
/usr/local/go/src/runtime/proc.go:250
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1571
Wrapped errors implement json.Marshaler interface. Grouped attributes are marshaled as nested objects.
Example
func main() {
err := errors.Errorf(
"sql error: %w", sql.ErrNoRows,
errors.String("sql", "SELECT id, name FROM product WHERE id = ?"),
errors.Int("productID", 123),
)
err = errors.Errorf(
"find product: %w", err,
errors.Group("request",
slog.String("id", "24874020-cab7-4ef3-bac5-76858832f8b0"),
slog.String("method", "GET"),
),
)
errJSON, err := json.MarshalIndent(err, "", "\t")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(errJSON))
}Output
{
"error": "find product: sql error: sql: no rows in result set",
"productID": 123,
"request": {
"id": "24874020-cab7-4ef3-bac5-76858832f8b0",
"method": "GET"
},
"sql": "SELECT id, name FROM product WHERE id = ?",
"stackTrace": [
{
"function": "main.main",
"file": "/home/user/project/main.go",
"line": 13
},
{
"function": "runtime.main",
"file": "/usr/local/go/src/runtime/proc.go",
"line": 250
},
{
"function": "runtime.goexit",
"file": "/usr/local/go/src/runtime/asm_amd64.s",
"line": 1571
}
]
}The package provides native integration with Go's log/slog. Errors implement slog.LogValuer,
so they work seamlessly with any slog logger.
err := errors.Errorf(
"database query failed: %w", dbErr,
errors.String("query", "SELECT * FROM users WHERE id = ?"),
errors.Int("userID", 123),
errors.Group("performance",
slog.Duration("duration", 250*time.Millisecond),
slog.Int("retries", 3),
),
)
// Log error at Error level with all attributes and stack trace
errors.Log(ctx, slog.Default(), err)err := errors.Errorf(
"operation failed: %w", someErr,
errors.String("operation", "user.create"),
errors.Int("userID", 123),
)
// Extract all attributes from error chain
attrs := errors.Attrs(err)
// Use with slog
slog.Error("request failed", append([]any{slog.Any("error", err)}, attrsToAny(attrs)...)...)Errors automatically work as slog.LogValuer, so you can log them directly:
err := errors.Wrap(
dbErr,
errors.String("table", "users"),
errors.Int("id", 123),
)
// The error will automatically provide its attributes to slog
slog.Error("database error", "error", err)You can implement errors.LoggableError interface on your custom error types to provide
structured attributes:
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed: %s", e.Message)
}
// Implement errors.LoggableError
func (e *ValidationError) Attrs() []slog.Attr {
return []slog.Attr{
slog.String("field", e.Field),
slog.Any("value", e.Value),
slog.String("validation_message", e.Message),
}
}
// Usage
err := &ValidationError{
Field: "email",
Value: "invalid-email",
Message: "must be a valid email address",
}
wrapped := errors.Wrap(err, errors.String("operation", "user.create"))
// All attributes from ValidationError will be included
attrs := errors.Attrs(wrapped)The package provides convenience functions for creating attributes that correspond to all slog types:
// Basic types
errors.Bool(key string, value bool)
errors.Int(key string, value int) // Converted to int64
errors.Int64(key string, value int64)
errors.Uint(key string, value uint) // Uses slog.Any
errors.Uint64(key string, value uint64)
errors.Float(key string, value float64) // Alias for Float64
errors.Float64(key string, value float64)
errors.String(key string, value string)
// Complex types
errors.Stringer(key string, value fmt.Stringer) // Converted to string
errors.Strings(key string, values []string) // Uses slog.Any
errors.Any(key string, value interface{}) // For any type
errors.Time(key string, value time.Time)
errors.Duration(key string, value time.Duration)
errors.JSON(key string, value json.RawMessage) // Uses slog.Any
// slog-specific options
errors.Attr(attr slog.Attr) // Add any slog.Attr directly
errors.WithAttrs(attrs ...slog.Attr) // Add multiple slog.Attr values
errors.Group(key string, attrs ...slog.Attr) // Create a grouped attribute
// Deprecated
errors.Value(key string, value interface{}) // Use Any insteadYou may help this project by
- reporting an issue;
- making translations for error messages;
- suggest an improvement or discuss the usability of the package.
If you'd like to contribute, see the contribution guide. Pull requests are welcome.
This project is licensed under the MIT License - see the LICENSE file for details.