From 41076380c47968a4199c5e613219a24b925c295b Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sat, 11 Apr 2026 12:30:23 +0800 Subject: [PATCH 1/2] feat: re-export stdlib errors functions for drop-in replacement Re-export Is, As, Unwrap, Join, and ErrUnsupported from the standard library so callers can use github.com/go-coldbrew/errors as their sole errors import without needing a separate "errors" import for stdlib functions. Also adds: - AsType generic re-export (behind go1.26 build tag) - Standalone Cause(err) function that walks the Unwrap chain - Updated package documentation and examples --- README.md | 297 ++++++++++++++++++++++++++++++++++++++++++--- doc.go | 46 ++++--- errors_test.go | 126 +++++++++++++++++-- example_test.go | 50 +++++++- notifier/README.md | 32 ++--- stdlib.go | 71 +++++++++++ stdlib_go126.go | 14 +++ 7 files changed, 576 insertions(+), 60 deletions(-) create mode 100644 stdlib.go create mode 100644 stdlib_go126.go diff --git a/README.md b/README.md index 92e6f38..21dd12b 100644 --- a/README.md +++ b/README.md @@ -13,28 +13,45 @@ import "github.com/go-coldbrew/errors" ``` -Package errors provides an implementation of golang error with stack strace information attached to it, the error objects created by this package are compatible with https://golang.org/pkg/errors/ +Package errors is a drop\-in replacement for the standard library "errors" package that adds stack trace capture, gRPC status codes, and error notification support. -How To Use The simplest way to use this package is by calling one of the two functions +All functions from the standard library errors package are re\-exported: [Is](<#Is>), [As](<#As>), [Unwrap](<#Unwrap>), [Join](<#Join>), and [ErrUnsupported](<#ErrUnsupported>). This allows you to use this package as your sole errors import: + +``` +import "github.com/go-coldbrew/errors" + +// Standard library functions work as expected: +errors.Is(err, target) +errors.As(err, &target) +errors.Unwrap(err) +errors.Join(err1, err2) + +// ColdBrew extensions add stack traces and gRPC status: +errors.New("something failed") // captures stack trace +errors.Wrap(err, "context") // wraps with stack trace +errors.Cause(err) // walks Unwrap chain to root cause +``` + +### Error Creation + +The simplest way to use this package is by calling one of the two functions: ``` errors.New(...) errors.Wrap(...) ``` -You can also initialize custom error stack by using one of the \`WithSkip\` functions. \`WithSkip\` allows skipping the defined number of functions from the stack information. +You can also initialize custom error stack by using one of the WithSkip functions. WithSkip allows skipping the defined number of functions from the stack information. ``` -if you want to create a new error use New -if you want to skip some functions on the stack use NewWithSkip -if you want to add GRPC status use NewWithStatus -if you want to skip some functions on the stack and add GRPC status use NewWithSkipAndStatus -if you want to wrap an existing error use Wrap -if you want to wrap an existing error and add GRPC status use WrapWithStatus -if you want to wrap an existing error and skip some functions on the stack use WrapWithSkip -if you want to wrap an existing error, skip some functions on the stack and add GRPC status use WrapWithSkipAndStatus -if you want to wrap an existing error and add notifier options use WrapWithNotifier -if you want to wrap an existing error, skip some functions on the stack and add notifier options use WrapWithSkipAndNotifier +New — create a new error with stack info +NewWithSkip — skip functions on the stack +NewWithStatus — add GRPC status +NewWithSkipAndStatus — skip functions and add GRPC status +Wrap — wrap an existing error +WrapWithStatus — wrap and add GRPC status +WrapWithSkip — wrap and skip functions on the stack +WrapWithSkipAndStatus — wrap, skip functions, and add GRPC status ``` Head to https://docs.coldbrew.cloud for more information. @@ -108,8 +125,15 @@ true ## Index - [Constants](<#constants>) +- [Variables](<#variables>) +- [func As\(err error, target any\) bool](<#As>) +- [func AsType\[E error\]\(err error\) \(E, bool\)](<#AsType>) +- [func Cause\(err error\) error](<#Cause>) +- [func Is\(err, target error\) bool](<#Is>) +- [func Join\(errs ...error\) error](<#Join>) - [func SetBaseFilePath\(path string\)](<#SetBaseFilePath>) - [func SetMaxStackDepth\(n int\)](<#SetMaxStackDepth>) +- [func Unwrap\(err error\) error](<#Unwrap>) - [type ErrorExt](<#ErrorExt>) - [func New\(msg string\) ErrorExt](<#New>) - [func NewWithSkip\(msg string, skip int\) ErrorExt](<#NewWithSkip>) @@ -133,6 +157,206 @@ true const SupportPackageIsVersion1 = true ``` +## Variables + +ErrUnsupported indicates that a requested operation cannot be performed, because it is unsupported. + +This is a re\-export of the standard library [errors.ErrUnsupported](<#ErrUnsupported>). + +```go +var ErrUnsupported = stderrors.ErrUnsupported +``` + + +## func [As]() + +```go +func As(err error, target any) bool +``` + +As finds the first error in err's tree that matches target, and if one is found, sets target to that error value and returns true. + +This is a re\-export of the standard library [errors.As](<#As>). + +
Example +

+ + + +```go +package main + +import ( + "fmt" + + "github.com/go-coldbrew/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func main() { + grpcErr := errors.NewWithStatus("not found", status.New(codes.NotFound, "not found")) + wrapped := errors.Wrap(grpcErr, "lookup failed") + + var ext errors.ErrorExt + if errors.As(wrapped, &ext) { + fmt.Println("found ErrorExt:", ext.GRPCStatus().Code()) + } +} +``` + +#### Output + +``` +found ErrorExt: NotFound +``` + +

+
+ + +## func [AsType]() + +```go +func AsType[E error](err error) (E, bool) +``` + +AsType finds the first error in err's tree that matches the type E, and if one is found, returns that error value and true. Otherwise, it returns the zero value of E and false. + +This is a re\-export of the standard library [errors.AsType](<#AsType>). + + +## func [Cause]() + +```go +func Cause(err error) error +``` + +Cause walks the [Unwrap](<#Unwrap>) chain of err and returns the innermost \(root cause\) error. If err does not implement Unwrap, err itself is returned. If err is nil, nil is returned. + +For [ErrorExt](<#ErrorExt>) errors, this produces the same result as calling the Cause method, but this function works on any error that implements the standard Unwrap interface. + +Note: for multi\-errors \(errors implementing Unwrap\(\) \[\]error, such as those created by [Join](<#Join>)\), the single\-error Unwrap returns nil, so Cause returns the multi\-error itself. + +
Example +

+ + + +```go +package main + +import ( + "fmt" + "io" + + "github.com/go-coldbrew/errors" +) + +func main() { + root := io.EOF + first := errors.Wrap(root, "read body") + second := errors.Wrap(first, "handle request") + fmt.Println(errors.Cause(second)) +} +``` + +#### Output + +``` +EOF +``` + +

+
+ + +## func [Is]() + +```go +func Is(err, target error) bool +``` + +Is reports whether any error in err's tree matches target. + +An error is considered a match if it is equal to the target or if it implements an Is\(error\) bool method such that Is\(target\) returns true. + +This is a re\-export of the standard library [errors.Is](<#Is>). + +
Example +

+ + + +```go +package main + +import ( + "fmt" + + "github.com/go-coldbrew/errors" +) + +func main() { + base := fmt.Errorf("connection refused") + wrapped := errors.Wrap(base, "dial failed") + fmt.Println(errors.Is(wrapped, base)) +} +``` + +#### Output + +``` +true +``` + +

+
+ + +## func [Join]() + +```go +func Join(errs ...error) error +``` + +Join returns an error that wraps the given errors. Any nil error values are discarded. Join returns nil if every value in errs is nil. + +This is a re\-export of the standard library [errors.Join](<#Join>). + +
Example +

+ + + +```go +package main + +import ( + "fmt" + + "github.com/go-coldbrew/errors" +) + +func main() { + err1 := errors.New("first") + err2 := errors.New("second") + joined := errors.Join(err1, err2) + fmt.Println(errors.Is(joined, err1)) + fmt.Println(errors.Is(joined, err2)) +} +``` + +#### Output + +``` +true +true +``` + +

+
+ ## func [SetBaseFilePath]() @@ -151,6 +375,48 @@ func SetMaxStackDepth(n int) SetMaxStackDepth sets the maximum number of stack frames captured when creating errors. Accepts values in \[1, 256\]; out\-of\-range values are ignored. Default is 16. Safe for concurrent use. + +## func [Unwrap]() + +```go +func Unwrap(err error) error +``` + +Unwrap returns the result of calling the Unwrap method on err, if err's type contains an Unwrap method returning error. Otherwise, Unwrap returns nil. + +This is a re\-export of the standard library [errors.Unwrap](<#Unwrap>). + +
Example +

+ + + +```go +package main + +import ( + "fmt" + "io" + + "github.com/go-coldbrew/errors" +) + +func main() { + base := io.EOF + wrapped := errors.Wrap(base, "read failed") + fmt.Println(errors.Unwrap(wrapped)) +} +``` + +#### Output + +``` +EOF +``` + +

+
+ ## type [ErrorExt]() @@ -319,13 +585,12 @@ cause: EOF
Example (Errors Is)

-Wrapped errors are compatible with stdlib errors.Is for unwrapping. +Wrapped errors are compatible with errors.Is for unwrapping. No separate "errors" import needed — Is is re\-exported. ```go package main import ( - stderrors "errors" "fmt" "io" @@ -335,7 +600,7 @@ import ( func main() { original := io.EOF wrapped := errors.Wrap(original, "read failed") - fmt.Println(stderrors.Is(wrapped, io.EOF)) + fmt.Println(errors.Is(wrapped, io.EOF)) } ``` diff --git a/doc.go b/doc.go index 378a648..abe2657 100644 --- a/doc.go +++ b/doc.go @@ -1,26 +1,42 @@ /* -Package errors provides an implementation of golang error with stack strace information attached to it, -the error objects created by this package are compatible with https://golang.org/pkg/errors/ +Package errors is a drop-in replacement for the standard library "errors" package +that adds stack trace capture, gRPC status codes, and error notification support. -How To Use -The simplest way to use this package is by calling one of the two functions +All functions from the standard library errors package are re-exported: +[Is], [As], [Unwrap], [Join], and [ErrUnsupported]. +This allows you to use this package as your sole errors import: + + import "github.com/go-coldbrew/errors" + + // Standard library functions work as expected: + errors.Is(err, target) + errors.As(err, &target) + errors.Unwrap(err) + errors.Join(err1, err2) + + // ColdBrew extensions add stack traces and gRPC status: + errors.New("something failed") // captures stack trace + errors.Wrap(err, "context") // wraps with stack trace + errors.Cause(err) // walks Unwrap chain to root cause + +# Error Creation + +The simplest way to use this package is by calling one of the two functions: errors.New(...) errors.Wrap(...) -You can also initialize custom error stack by using one of the `WithSkip` functions. `WithSkip` allows +You can also initialize custom error stack by using one of the WithSkip functions. WithSkip allows skipping the defined number of functions from the stack information. - if you want to create a new error use New - if you want to skip some functions on the stack use NewWithSkip - if you want to add GRPC status use NewWithStatus - if you want to skip some functions on the stack and add GRPC status use NewWithSkipAndStatus - if you want to wrap an existing error use Wrap - if you want to wrap an existing error and add GRPC status use WrapWithStatus - if you want to wrap an existing error and skip some functions on the stack use WrapWithSkip - if you want to wrap an existing error, skip some functions on the stack and add GRPC status use WrapWithSkipAndStatus - if you want to wrap an existing error and add notifier options use WrapWithNotifier - if you want to wrap an existing error, skip some functions on the stack and add notifier options use WrapWithSkipAndNotifier + New — create a new error with stack info + NewWithSkip — skip functions on the stack + NewWithStatus — add GRPC status + NewWithSkipAndStatus — skip functions and add GRPC status + Wrap — wrap an existing error + WrapWithStatus — wrap and add GRPC status + WrapWithSkip — wrap and skip functions on the stack + WrapWithSkipAndStatus — wrap, skip functions, and add GRPC status Head to https://docs.coldbrew.cloud for more information. */ diff --git a/errors_test.go b/errors_test.go index f38e8c2..578c7e7 100644 --- a/errors_test.go +++ b/errors_test.go @@ -9,7 +9,7 @@ import ( ) func TestWrap(t *testing.T) { - var tests = []struct { + tests := []struct { name string err error message string @@ -39,7 +39,6 @@ func TestWrap(t *testing.T) { if grpcstatus.Convert(err).Message() != tt.expected { t.Errorf("GRPC status msg expected %+v, got %+v", tt.expected, grpcstatus.Convert(err).Message()) } - }) } } @@ -51,22 +50,22 @@ func TestErrorsIs(t *testing.T) { wrapped2 := Wrap(wrapped1, "layer2") wrapped3 := Wrap(wrapped2, "layer3") - if !stderrors.Is(wrapped1, base) { + if !Is(wrapped1, base) { t.Error("wrapped1 should match base via errors.Is") } - if !stderrors.Is(wrapped2, wrapped1) { + if !Is(wrapped2, wrapped1) { t.Error("wrapped2 should match wrapped1 via errors.Is") } - if !stderrors.Is(wrapped2, base) { + if !Is(wrapped2, base) { t.Error("wrapped2 should match base via errors.Is") } - if !stderrors.Is(wrapped3, wrapped2) { + if !Is(wrapped3, wrapped2) { t.Error("wrapped3 should match wrapped2 via errors.Is") } - if !stderrors.Is(wrapped3, wrapped1) { + if !Is(wrapped3, wrapped1) { t.Error("wrapped3 should match wrapped1 via errors.Is") } - if !stderrors.Is(wrapped3, base) { + if !Is(wrapped3, base) { t.Error("wrapped3 should match base via errors.Is") } } @@ -99,7 +98,7 @@ func TestWrapf(t *testing.T) { if err.Error() != expected { t.Errorf("Wrapf() = %q, want %q", err.Error(), expected) } - if !stderrors.Is(err, base) { + if !Is(err, base) { t.Error("Wrapf result should match base via errors.Is") } } @@ -169,6 +168,114 @@ func TestSetMaxStackDepth(t *testing.T) { } } +func TestIs(t *testing.T) { + base := stderrors.New("base") + wrapped := Wrap(base, "wrapped") + + if !Is(wrapped, base) { + t.Error("Is should find base through ColdBrew wrap") + } + if Is(wrapped, stderrors.New("other")) { + t.Error("Is should not match unrelated error") + } + if !Is(wrapped, wrapped) { + t.Error("Is should match error against itself") + } +} + +func TestAs(t *testing.T) { + grpcErr := NewWithStatus("not found", grpcstatus.New(5, "not found")) + wrapped := Wrap(grpcErr, "lookup failed") + + var ext ErrorExt + if !As(wrapped, &ext) { + t.Fatal("As should find ErrorExt in chain") + } + if ext.Error() != "lookup failed: not found" { + t.Errorf("As target = %q, want %q", ext.Error(), "lookup failed: not found") + } +} + +func TestUnwrap(t *testing.T) { + base := stderrors.New("base") + wrapped := Wrap(base, "ctx") + + unwrapped := Unwrap(wrapped) + if unwrapped == nil { + t.Fatal("Unwrap should return non-nil for wrapped error") + } + if unwrapped != base { + t.Errorf("Unwrap = %v, want %v", unwrapped, base) + } + + // Unwrap on a non-wrapping error returns nil + plain := stderrors.New("plain") + if Unwrap(plain) != nil { + t.Error("Unwrap on non-wrapping error should return nil") + } +} + +func TestJoin(t *testing.T) { + err1 := New("first") + err2 := stderrors.New("second") + + joined := Join(err1, err2) + if joined == nil { + t.Fatal("Join should return non-nil") + } + if !Is(joined, err1) { + t.Error("Join result should contain first error") + } + if !Is(joined, err2) { + t.Error("Join result should contain second error") + } + + // All nils returns nil + if Join(nil, nil) != nil { + t.Error("Join of all nils should return nil") + } +} + +func TestErrUnsupported(t *testing.T) { + err := stderrors.ErrUnsupported + if !Is(err, ErrUnsupported) { + t.Error("ErrUnsupported should match stdlib ErrUnsupported via Is") + } +} + +func TestCauseStandalone(t *testing.T) { + // Works on ColdBrew error chain + root := stderrors.New("root") + a := Wrap(root, "a") + b := Wrap(a, "b") + + if got := Cause(b); got != root { + t.Errorf("Cause(b) = %v, want %v", got, root) + } + if got := Cause(a); got != root { + t.Errorf("Cause(a) = %v, want %v", got, root) + } + + // Works on plain stdlib errors (no Unwrap) + plain := stderrors.New("plain") + if got := Cause(plain); got != plain { + t.Errorf("Cause(plain) = %v, want %v", got, plain) + } + + // Returns nil for nil + if got := Cause(nil); got != nil { + t.Errorf("Cause(nil) = %v, want nil", got) + } + + // Works on stdlib fmt.Errorf chains + import_io_err := io.EOF + fmtWrapped := stderrors.Join(import_io_err) + // Join uses Unwrap() []error, not Unwrap() error, so Cause returns itself + if got := Cause(fmtWrapped); got != fmtWrapped { + t.Errorf("Cause(joinedErr) = %v, want %v", got, fmtWrapped) + } +} + func TestStackFrameConsistency(t *testing.T) { err := New("consistency test") @@ -196,4 +303,3 @@ func TestStackFrameConsistency(t *testing.T) { t.Fatalf("StackFrame count %d exceeds Callers count %d", len(frames1), len(pcs)) } } - diff --git a/example_test.go b/example_test.go index 2593ea6..0366254 100644 --- a/example_test.go +++ b/example_test.go @@ -1,7 +1,6 @@ package errors_test import ( - stderrors "errors" "fmt" "io" @@ -73,10 +72,55 @@ func Example_cause() { // cause: EOF } -// Wrapped errors are compatible with stdlib errors.Is for unwrapping. +// Wrapped errors are compatible with errors.Is for unwrapping. +// No separate "errors" import needed — Is is re-exported. func ExampleWrap_errorsIs() { original := io.EOF wrapped := errors.Wrap(original, "read failed") - fmt.Println(stderrors.Is(wrapped, io.EOF)) + fmt.Println(errors.Is(wrapped, io.EOF)) // Output: true } + +func ExampleIs() { + base := fmt.Errorf("connection refused") + wrapped := errors.Wrap(base, "dial failed") + fmt.Println(errors.Is(wrapped, base)) + // Output: true +} + +func ExampleAs() { + grpcErr := errors.NewWithStatus("not found", status.New(codes.NotFound, "not found")) + wrapped := errors.Wrap(grpcErr, "lookup failed") + + var ext errors.ErrorExt + if errors.As(wrapped, &ext) { + fmt.Println("found ErrorExt:", ext.GRPCStatus().Code()) + } + // Output: found ErrorExt: NotFound +} + +func ExampleJoin() { + err1 := errors.New("first") + err2 := errors.New("second") + joined := errors.Join(err1, err2) + fmt.Println(errors.Is(joined, err1)) + fmt.Println(errors.Is(joined, err2)) + // Output: + // true + // true +} + +func ExampleUnwrap() { + base := io.EOF + wrapped := errors.Wrap(base, "read failed") + fmt.Println(errors.Unwrap(wrapped)) + // Output: EOF +} + +func ExampleCause() { + root := io.EOF + first := errors.Wrap(root, "read body") + second := errors.Wrap(first, "handle request") + fmt.Println(errors.Cause(second)) + // Output: EOF +} diff --git a/notifier/README.md b/notifier/README.md index 1414300..b3f511f 100755 --- a/notifier/README.md +++ b/notifier/README.md @@ -41,7 +41,7 @@ import "github.com/go-coldbrew/errors/notifier" -## func [Close]() +## func [Close]() ```go func Close() @@ -59,7 +59,7 @@ func GetTraceHeaderName() string GetTraceHeaderName gets the header name for trace id default is x\-trace\-id -## func [GetTraceId]() +## func [GetTraceId]() ```go func GetTraceId(ctx context.Context) string @@ -86,7 +86,7 @@ func InitRollbar(token, env string) InitRollbar inits rollbar configuration token: rollbar token env: rollbar environment -## func [InitSentry]() +## func [InitSentry]() ```go func InitSentry(dsn string) @@ -95,7 +95,7 @@ func InitSentry(dsn string) InitSentry inits sentry configuration dsn: sentry dsn -## func [Notify]() +## func [Notify]() ```go func Notify(err error, rawData ...interface{}) error @@ -113,7 +113,7 @@ func NotifyAsync(err error, rawData ...interface{}) error NotifyAsync sends an error notification asynchronously with bounded concurrency. If the async notification pool is full, the notification is dropped to prevent goroutine explosion under sustained error bursts. Returns the original error for convenience. -## func [NotifyOnPanic]() +## func [NotifyOnPanic]() ```go func NotifyOnPanic(rawData ...interface{}) @@ -122,7 +122,7 @@ func NotifyOnPanic(rawData ...interface{}) NotifyOnPanic notifies error to airbrake, rollbar and sentry if they are inited and error is not ignored rawData: extra data to notify with error \(can be context.Context, Tags, or any other data\) when rawData is context.Context, it will used to get extra data from loggers.FromContext\(ctx\) and tags from metadata this function should be called in defer example: defer NotifyOnPanic\(ctx, "some data"\) example: defer NotifyOnPanic\(ctx, "some data", Tags\{"tag1": "value1"\}\) -## func [NotifyWithExclude]() +## func [NotifyWithExclude]() ```go func NotifyWithExclude(err error, rawData ...interface{}) error @@ -131,7 +131,7 @@ func NotifyWithExclude(err error, rawData ...interface{}) error NotifyWithExclude notifies error to airbrake, rollbar and sentry if they are inited and error is not ignored err: error to notify rawData: extra data to notify with error \(can be context.Context, Tags, or any other data\) when rawData is context.Context, it will used to get extra data from loggers.FromContext\(ctx\) and tags from metadata -## func [NotifyWithLevel]() +## func [NotifyWithLevel]() ```go func NotifyWithLevel(err error, level string, rawData ...interface{}) error @@ -140,7 +140,7 @@ func NotifyWithLevel(err error, level string, rawData ...interface{}) error NotifyWithLevel notifies error to airbrake, rollbar and sentry if they are inited and error is not ignored err: error to notify level: error level rawData: extra data to notify with error \(can be context.Context, Tags, or any other data\) when rawData is context.Context, it will used to get extra data from loggers.FromContext\(ctx\) and tags from metadata -## func [NotifyWithLevelAndSkip]() +## func [NotifyWithLevelAndSkip]() ```go func NotifyWithLevelAndSkip(err error, skip int, level string, rawData ...interface{}) error @@ -149,7 +149,7 @@ func NotifyWithLevelAndSkip(err error, skip int, level string, rawData ...interf NotifyWithLevelAndSkip notifies error to airbrake, rollbar and sentry if they are inited and error is not ignored err: error to notify skip: skip stack frames when notify error level: error level rawData: extra data to notify with error \(can be context.Context, Tags, or any other data\) when rawData is context.Context, it will used to get extra data from loggers.FromContext\(ctx\) and tags from metadata -## func [SetEnvironment]() +## func [SetEnvironment]() ```go func SetEnvironment(env string) @@ -158,7 +158,7 @@ func SetEnvironment(env string) SetEnvironment sets the environment. The environment is used to distinguish errors occurring in different -## func [SetHostname]() +## func [SetHostname]() ```go func SetHostname(name string) @@ -176,7 +176,7 @@ func SetMaxAsyncNotifications(n int) SetMaxAsyncNotifications sets the maximum number of concurrent async notification goroutines. When the limit is reached, new async notifications are dropped to prevent goroutine explosion under sustained error bursts. Default is 20. The first successful call wins; subsequent calls are no\-ops. It is safe to call concurrently with NotifyAsync. -## func [SetRelease]() +## func [SetRelease]() ```go func SetRelease(rel string) @@ -185,7 +185,7 @@ func SetRelease(rel string) SetRelease sets the release tag. The release tag is used to group errors together by release. -## func [SetServerRoot]() +## func [SetServerRoot]() ```go func SetServerRoot(path string) @@ -203,7 +203,7 @@ func SetTraceHeaderName(name string) SetTraceHeaderName sets the header name for trace id default is x\-trace\-id -## func [SetTraceIDValidator]() +## func [SetTraceIDValidator]() ```go func SetTraceIDValidator(fn func(string) string) @@ -212,7 +212,7 @@ func SetTraceIDValidator(fn func(string) string) SetTraceIDValidator sets a custom trace ID validation function. The function receives a raw trace ID and must return the sanitized version. Returning an empty string triggers the standard trace ID resolution flow \(existing ctx → gRPC metadata → OTEL span trace ID → generate UUID\), not direct generation. Set to nil to disable validation entirely \(not recommended\). Must be called during init — not safe for concurrent use. -## func [SetTraceId]() +## func [SetTraceId]() ```go func SetTraceId(ctx context.Context) context.Context @@ -221,7 +221,7 @@ func SetTraceId(ctx context.Context) context.Context SetTraceId updates the traceID based on context values if no trace id is found then it will create one and update the context You should use the context returned by this function instead of the one passed -## func [SetTraceIdWithValue]() +## func [SetTraceIdWithValue]() ```go func SetTraceIdWithValue(ctx context.Context) (context.Context, string) @@ -230,7 +230,7 @@ func SetTraceIdWithValue(ctx context.Context) (context.Context, string) SetTraceIdWithValue is like SetTraceId but also returns the resolved trace ID, avoiding a separate GetTraceId call. Callers must use the returned context, not the original ctx, so the stored trace ID is preserved in options and log context. -## func [UpdateTraceId]() +## func [UpdateTraceId]() ```go func UpdateTraceId(ctx context.Context, traceID string) context.Context diff --git a/stdlib.go b/stdlib.go new file mode 100644 index 0000000..36247c9 --- /dev/null +++ b/stdlib.go @@ -0,0 +1,71 @@ +package errors + +import stderrors "errors" + +// --- Re-exports from the standard library errors package --- +// +// These allow github.com/go-coldbrew/errors to be used as a drop-in +// replacement for the standard "errors" package. + +// Is reports whether any error in err's tree matches target. +// +// An error is considered a match if it is equal to the target or if +// it implements an Is(error) bool method such that Is(target) returns true. +// +// This is a re-export of the standard library [errors.Is]. +func Is(err, target error) bool { + return stderrors.Is(err, target) +} + +// As finds the first error in err's tree that matches target, +// and if one is found, sets target to that error value and returns true. +// +// This is a re-export of the standard library [errors.As]. +func As(err error, target any) bool { + return stderrors.As(err, target) +} + +// Unwrap returns the result of calling the Unwrap method on err, +// if err's type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +// +// This is a re-export of the standard library [errors.Unwrap]. +func Unwrap(err error) error { + return stderrors.Unwrap(err) +} + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. Join returns nil if every value +// in errs is nil. +// +// This is a re-export of the standard library [errors.Join]. +func Join(errs ...error) error { + return stderrors.Join(errs...) +} + +// ErrUnsupported indicates that a requested operation cannot be performed, +// because it is unsupported. +// +// This is a re-export of the standard library [errors.ErrUnsupported]. +var ErrUnsupported = stderrors.ErrUnsupported + +// Cause walks the [Unwrap] chain of err and returns the innermost +// (root cause) error. If err does not implement Unwrap, err itself +// is returned. If err is nil, nil is returned. +// +// For [ErrorExt] errors, this produces the same result as calling +// the Cause method, but this function works on any error that +// implements the standard Unwrap interface. +// +// Note: for multi-errors (errors implementing Unwrap() []error, such as +// those created by [Join]), the single-error Unwrap returns nil, so +// Cause returns the multi-error itself. +func Cause(err error) error { + for { + unwrapped := stderrors.Unwrap(err) + if unwrapped == nil { + return err + } + err = unwrapped + } +} diff --git a/stdlib_go126.go b/stdlib_go126.go new file mode 100644 index 0000000..267ed09 --- /dev/null +++ b/stdlib_go126.go @@ -0,0 +1,14 @@ +//go:build go1.26 + +package errors + +import stderrors "errors" + +// AsType finds the first error in err's tree that matches the type E, +// and if one is found, returns that error value and true. +// Otherwise, it returns the zero value of E and false. +// +// This is a re-export of the standard library [errors.AsType]. +func AsType[E error](err error) (E, bool) { + return stderrors.AsType[E](err) +} From a4a9db45d0a7fbdf7f7e71e41bf7a5c3e1419694 Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sat, 11 Apr 2026 13:11:45 +0800 Subject: [PATCH 2/2] fix: address PR review feedback - Add bounded traversal (max 1024) to Cause to guard against cyclic Unwrap chains - Fix doc comments to avoid ambiguous [errors.X] self-links - Reword "all functions" claim to accurately describe re-exported subset - Fix misleading test comment (Join multi-error, not fmt.Errorf chain) - Regenerate README --- README.md | 16 ++++++++-------- doc.go | 4 ++-- errors_test.go | 11 +++++------ stdlib.go | 17 +++++++++++------ stdlib_go126.go | 2 +- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 21dd12b..e6dc8fc 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ import "github.com/go-coldbrew/errors" Package errors is a drop\-in replacement for the standard library "errors" package that adds stack trace capture, gRPC status codes, and error notification support. -All functions from the standard library errors package are re\-exported: [Is](<#Is>), [As](<#As>), [Unwrap](<#Unwrap>), [Join](<#Join>), and [ErrUnsupported](<#ErrUnsupported>). This allows you to use this package as your sole errors import: +The following standard library helpers are re\-exported: [Is](<#Is>), [As](<#As>), [Unwrap](<#Unwrap>), [Join](<#Join>), and the [ErrUnsupported](<#ErrUnsupported>) sentinel. This allows you to use this package as your sole errors import: ``` import "github.com/go-coldbrew/errors" @@ -161,7 +161,7 @@ const SupportPackageIsVersion1 = true ErrUnsupported indicates that a requested operation cannot be performed, because it is unsupported. -This is a re\-export of the standard library [errors.ErrUnsupported](<#ErrUnsupported>). +Re\-exported from the standard library errors package. ```go var ErrUnsupported = stderrors.ErrUnsupported @@ -176,7 +176,7 @@ func As(err error, target any) bool As finds the first error in err's tree that matches target, and if one is found, sets target to that error value and returns true. -This is a re\-export of the standard library [errors.As](<#As>). +Re\-exported from the standard library errors package.

Example

@@ -223,10 +223,10 @@ func AsType[E error](err error) (E, bool) AsType finds the first error in err's tree that matches the type E, and if one is found, returns that error value and true. Otherwise, it returns the zero value of E and false. -This is a re\-export of the standard library [errors.AsType](<#AsType>). +Re\-exported from the standard library errors package \(requires Go 1.26\+\). -## func [Cause]() +## func [Cause]() ```go func Cause(err error) error @@ -281,7 +281,7 @@ Is reports whether any error in err's tree matches target. An error is considered a match if it is equal to the target or if it implements an Is\(error\) bool method such that Is\(target\) returns true. -This is a re\-export of the standard library [errors.Is](<#Is>). +Re\-exported from the standard library errors package.

Example

@@ -322,7 +322,7 @@ func Join(errs ...error) error Join returns an error that wraps the given errors. Any nil error values are discarded. Join returns nil if every value in errs is nil. -This is a re\-export of the standard library [errors.Join](<#Join>). +Re\-exported from the standard library errors package.

Example

@@ -384,7 +384,7 @@ func Unwrap(err error) error Unwrap returns the result of calling the Unwrap method on err, if err's type contains an Unwrap method returning error. Otherwise, Unwrap returns nil. -This is a re\-export of the standard library [errors.Unwrap](<#Unwrap>). +Re\-exported from the standard library errors package.

Example

diff --git a/doc.go b/doc.go index abe2657..c15668f 100644 --- a/doc.go +++ b/doc.go @@ -2,8 +2,8 @@ Package errors is a drop-in replacement for the standard library "errors" package that adds stack trace capture, gRPC status codes, and error notification support. -All functions from the standard library errors package are re-exported: -[Is], [As], [Unwrap], [Join], and [ErrUnsupported]. +The following standard library helpers are re-exported: +[Is], [As], [Unwrap], [Join], and the [ErrUnsupported] sentinel. This allows you to use this package as your sole errors import: import "github.com/go-coldbrew/errors" diff --git a/errors_test.go b/errors_test.go index 578c7e7..0ccca95 100644 --- a/errors_test.go +++ b/errors_test.go @@ -267,12 +267,11 @@ func TestCauseStandalone(t *testing.T) { t.Errorf("Cause(nil) = %v, want nil", got) } - // Works on stdlib fmt.Errorf chains - import_io_err := io.EOF - fmtWrapped := stderrors.Join(import_io_err) - // Join uses Unwrap() []error, not Unwrap() error, so Cause returns itself - if got := Cause(fmtWrapped); got != fmtWrapped { - t.Errorf("Cause(joinedErr) = %v, want %v", got, fmtWrapped) + // Join multi-errors use Unwrap() []error, not Unwrap() error, + // so single-error Unwrap returns nil and Cause returns the Join itself. + joined := stderrors.Join(io.EOF) + if got := Cause(joined); got != joined { + t.Errorf("Cause(joinedErr) = %v, want %v", got, joined) } } diff --git a/stdlib.go b/stdlib.go index 36247c9..3f5dd3d 100644 --- a/stdlib.go +++ b/stdlib.go @@ -12,7 +12,7 @@ import stderrors "errors" // An error is considered a match if it is equal to the target or if // it implements an Is(error) bool method such that Is(target) returns true. // -// This is a re-export of the standard library [errors.Is]. +// Re-exported from the standard library errors package. func Is(err, target error) bool { return stderrors.Is(err, target) } @@ -20,7 +20,7 @@ func Is(err, target error) bool { // As finds the first error in err's tree that matches target, // and if one is found, sets target to that error value and returns true. // -// This is a re-export of the standard library [errors.As]. +// Re-exported from the standard library errors package. func As(err error, target any) bool { return stderrors.As(err, target) } @@ -29,7 +29,7 @@ func As(err error, target any) bool { // if err's type contains an Unwrap method returning error. // Otherwise, Unwrap returns nil. // -// This is a re-export of the standard library [errors.Unwrap]. +// Re-exported from the standard library errors package. func Unwrap(err error) error { return stderrors.Unwrap(err) } @@ -38,7 +38,7 @@ func Unwrap(err error) error { // Any nil error values are discarded. Join returns nil if every value // in errs is nil. // -// This is a re-export of the standard library [errors.Join]. +// Re-exported from the standard library errors package. func Join(errs ...error) error { return stderrors.Join(errs...) } @@ -46,9 +46,13 @@ func Join(errs ...error) error { // ErrUnsupported indicates that a requested operation cannot be performed, // because it is unsupported. // -// This is a re-export of the standard library [errors.ErrUnsupported]. +// Re-exported from the standard library errors package. var ErrUnsupported = stderrors.ErrUnsupported +// maxCauseDepth is the upper bound on Unwrap iterations in [Cause] +// to guard against cyclic Unwrap chains. +const maxCauseDepth = 1024 + // Cause walks the [Unwrap] chain of err and returns the innermost // (root cause) error. If err does not implement Unwrap, err itself // is returned. If err is nil, nil is returned. @@ -61,11 +65,12 @@ var ErrUnsupported = stderrors.ErrUnsupported // those created by [Join]), the single-error Unwrap returns nil, so // Cause returns the multi-error itself. func Cause(err error) error { - for { + for range maxCauseDepth { unwrapped := stderrors.Unwrap(err) if unwrapped == nil { return err } err = unwrapped } + return err } diff --git a/stdlib_go126.go b/stdlib_go126.go index 267ed09..98be135 100644 --- a/stdlib_go126.go +++ b/stdlib_go126.go @@ -8,7 +8,7 @@ import stderrors "errors" // and if one is found, returns that error value and true. // Otherwise, it returns the zero value of E and false. // -// This is a re-export of the standard library [errors.AsType]. +// Re-exported from the standard library errors package (requires Go 1.26+). func AsType[E error](err error) (E, bool) { return stderrors.AsType[E](err) }