Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

26 Commits

Repository files navigation

errors

Go ReferenceGitHub go.mod Go versionGitHub release (latest by date)GitHubtestsGo Report CardMaintainabilityContributor Covenant

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 native log/slog integration.
If you're migrating from v0.4.1, please read the Migration Guide.

Key features

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 use slog.Attr;
  • supports grouped attributes via slog.Group;
  • implements slog.LogValuer for seamless slog integration;
  • package errors can be easily marshaled into JSON with all fields in a chain.

Additional features

  • errors.IsOfType[T any](err error) to test for error types.
  • errors.Attrs(err error) []slog.Attr to extract all attributes from error chain.
  • errors.Log(ctx, logger, err) and errors.LogLevel(ctx, logger, level, err) for logging with slog.

Installation

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.

How to use

errors.New() for package-level errors

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() for wrapping errors with formatted message, attributes and stack trace

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() for wrapping errors with attributes and stack trace

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
)
}

Working with slog attributes

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",
}),
)

Working with grouped attributes

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"),
),
)

Printing error with stack trace

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

Marshal error into JSON

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
}
]
}

Structured logging with slog

The package provides native integration with Go's log/slog. Errors implement slog.LogValuer, so they work seamlessly with any slog logger.

Using Log convenience function

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)

Extracting attributes manually

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)...)...)

Using slog.LogValuer

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)

Custom LoggableError types

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)

Available attribute options

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 instead

Contributing

You 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.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Releases

Packages

Used by

Contributors

Languages