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.
varErrNotFound=errors.New("not found")
varerrInternalError=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.funcNewNotFoundError() error {
returnerrors.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)
varproductProducterr:=row.Scan(&product.ID, &product.Name)
iferr!=nil {
// Option 1: Use helper functionsreturnnil, errors.Errorf(
"%w: %v", errSQLError, err.Error(),
errors.String("sql", findSQL),
errors.Int("productID", id),
)
// Option 2: Use slog.Attr directly (more idiomatic)returnnil, 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)
iferr!=nil {
// Option 1: Use helper functionsreturnnil, errors.Wrap(
err,
errors.Int("userID", userID),
errors.String("userMessage", message),
)
// Option 2: Use slog.Attr directly (recommended)returnnil, errors.Wrap(
err,
slog.Int("userID", userID),
slog.String("userMessage", message),
)
// Option 3: Mix both stylesreturnnil, errors.Wrap(
err,
errors.SkipCaller(), // Option for stack traceslog.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 supportederr:=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 helpererr:=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
funcmain() {
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
funcmain() {
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")
iferr!=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 traceerrors.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 chainattrs:=errors.Attrs(err)
// Use with slogslog.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 slogslog.Error("database error", "error", err)You can implement errors.LoggableError interface on your custom error types to provide
structured attributes:
typeValidationErrorstruct {
FieldstringValueinterface{}
Messagestring
}
func (e*ValidationError) Error() string {
returnfmt.Sprintf("validation failed: %s", e.Message)
}
// Implement errors.LoggableErrorfunc (e*ValidationError) Attrs() []slog.Attr {
return []slog.Attr{
slog.String("field", e.Field),
slog.Any("value", e.Value),
slog.String("validation_message", e.Message),
}
}
// Usageerr:=&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 includedattrs:=errors.Attrs(wrapped)The package provides convenience functions for creating attributes that correspond to all slog types:
// Basic typeserrors.Bool(keystring, valuebool)
errors.Int(keystring, valueint) // Converted to int64errors.Int64(keystring, valueint64)
errors.Uint(keystring, valueuint) // Uses slog.Anyerrors.Uint64(keystring, valueuint64)
errors.Float(keystring, valuefloat64) // Alias for Float64errors.Float64(keystring, valuefloat64)
errors.String(keystring, valuestring)
// Complex typeserrors.Stringer(keystring, valuefmt.Stringer) // Converted to stringerrors.Strings(keystring, values []string) // Uses slog.Any
errors.Any(keystring, valueinterface{}) // For any typeerrors.Time(keystring, valuetime.Time)
errors.Duration(keystring, valuetime.Duration)
errors.JSON(keystring, valuejson.RawMessage) // Uses slog.Any// slog-specific optionserrors.Attr(attrslog.Attr) // Add any slog.Attr directlyerrors.WithAttrs(attrs...slog.Attr) // Add multiple slog.Attr valueserrors.Group(keystring, attrs...slog.Attr) // Create a grouped attribute// Deprecatederrors.Value(keystring, valueinterface{}) // 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.