errorc is a minimalistic extension to Go's standard error type, providing additional structured context. Written by ygrebnov.
The errorc.With function behaves like fmt.Errorf, but performs significantly faster in benchmarks:
BenchmarkWith-8 53965288 21.81 ns/op
BenchmarkFmtErrorf-8 7401583 186.7 ns/op
The With function allows wrapping a sentinel error with additional context and later identifying this error using errors.Is.
// Create a new named error.
ErrInvalidInput := errorc.New("invalid input")
// Wrap the named error with additional context.
err := errorc.With(
ErrInvalidInput,
errorc.String("field1", "value1"),
errorc.String("field2", "value2"),
)
// Identify the error using errors.Is.
if errors.Is(err, ErrInvalidInput) {
// Handle the invalid input error.
fmt.Print("Handled invalid input error: ", err.Error())
}The With function allows wrapping a typed error with additional context and later identifying this error using errors.As.
type ValidationError struct { Message string }
func (e *ValidationError) Error() string { return e.Message }
err := errorc.With(
&ValidationError{"invalid input"},
errorc.String("field1", "value1"),
errorc.String("field2", "value2"),
)
// Identify ValidationError using errors.As.
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Print("Handled ValidationError: ", err.Error())
}String is generic: func String[K ~string](key K, value string). This lets you define strongly typed keys without manual casting:
type Key string
const (
UserID Key = "user_id"
TraceID Key = "trace_id"
)
err := errorc.With(
errorc.New("invalid input"),
errorc.String(UserID, "123"),
errorc.String(TraceID, "abc-xyz"),
)
fmt.Println(err) // invalid input, user_id: 123, trace_id: abc-xyzYou can still pass plain string keys; type inference picks K = string automatically:
err := errorc.With(errorc.New("oops"), errorc.String("detail", "something"))
fmt.Println(err)String, Int, Bool, and Error also work with github.com/ygrebnov/keys.Key, since it has an underlying string type:
userIDKey := keys.New("id", keys.WithSegments("user"))
err := errorc.With(errorc.New("invalid input"), errorc.String(userIDKey, "123"))
fmt.Println(err) // invalid input, user.id: 123Use Error to capture another error's message as a structured field. Nil errors are ignored.
cause := errors.New("disk full")
err := errorc.With(errorc.New("operation failed"), errorc.Error("cause", cause))
fmt.Println(err) // operation failed, cause: disk full
// Empty key prints only the inner error's message
err2 := errorc.With(errorc.New("operation failed"), errorc.Error("", cause))
fmt.Println(err2) // operation failed, disk full
// Nil cause is skipped
err3 := errorc.With(errorc.New("operation failed"), errorc.Error("cause", nil))
fmt.Println(err3) // operation failedError deliberately stores only the wrapped error's message as a formatted field. It does not add that error to the Go error chain, so errors.Is and errors.As will continue to inspect the base error passed to With, not errors embedded as fields.
This keeps errorc lightweight and avoids treating diagnostic context as semantic error wrapping. If an underlying error should be discoverable through errors.Is or errors.As, wrap or join it explicitly before passing it to With.
Structured keys are provided by github.com/ygrebnov/keys.
Use keys.New or keys.Factory there, then pass the resulting key to errorc.String,
errorc.Int, errorc.Bool, or errorc.Error.
// Direct construction.
userIDKey := keys.New("id", keys.WithSegments("user"))
// Pre-bound constructor for shared segments.
userKey := keys.Factory(keys.WithSegments("user"))
emailKey := userKey("email")
err := errorc.With(
errorc.New("invalid input"),
errorc.String(userIDKey, "123"),
errorc.String(emailKey, "user@example.com"),
)
fmt.Println(err) // invalid input, user.id: 123, user.email: user@example.comMigration snippet:
// Before (old errorc key helpers)
// userKey := errorc.NewKey("id", errorc.WithSegments("user"))
// After (keys)
userKey := keys.New("id", keys.WithSegments("user"))Helpers for common primitive types. These convert the value once when the field is created (no repeated formatting) and follow the same formatting rules (empty key prints only the value):
err := errorc.With(
errorc.New("query failed"),
errorc.Int("retries", 3),
errorc.Bool("cached", false),
)
fmt.Println(err) // query failed, retries: 3, cached: false
// Empty keys -> just values
err2 := errorc.With(errorc.New("status"), errorc.Int("", 10), errorc.Bool("", true))
fmt.Println(err2) // status, 10, trueGiven a base error E and fields F1..Fn:
- Empty key & non-empty value -> appended as
value - Non-empty key & any value -> appended as
key: value - Empty key & empty value -> omitted (no bytes appended)
The final error string is: E.Error(), <field1>, <field2>, ... (comma+space separated) for each non-nil field.
You can construct simple, namespaced error identifiers using New together with
WithNamespace, or via Namespace.NewError / ErrorFactory:
// Using New and WithNamespace
err := errorc.New("read_failed", errorc.WithNamespace("storage"))
fmt.Println(err) // storage: read_failed
// Using a Namespace method
storage := errorc.Namespace("storage")
err2 := storage.NewError("read_failed")
fmt.Println(err2) // storage: read_failed
// Using ErrorFactory
storageErr := errorc.ErrorFactory("storage")
err3 := storageErr("read_failed")
fmt.Println(err3) // storage: read_failedIf the message is empty and the namespace is non-empty, both Namespace.NewError("")
and ErrorFactory(...)("") produce an error string that contains only the
namespace prefix, for example "storage: ".
For structured keys such as segment1.segment2.name, use github.com/ygrebnov/keys.
Compatible with Go 1.22 or later:
go get github.com/ygrebnov/errorcThis library is pre-1.0; minor version bumps (e.g. 0.2.0) may include breaking changes. Once it reaches 1.0, semantic versioning will apply more strictly.
Contributions are welcome!
Please open an issue or submit a pull request.
Distributed under the BSD 3-Clause License. See the LICENSE file for details.