diff --git a/README.md b/README.md index 4cf393d..843aad9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,393 @@ -# `errors` +# errors -This package provides a drop-in replacement for `errors` (stdlib) & `github.com/pkg/errors` with stacktrace embedding, tagging, etc. +A comprehensive Go error handling library that extends the standard library with rich context, stack traces, error chaining, and integration with error reporting services. -Sub-packages also provides testing facilities, multiple errors handling, etc. +This package provides a drop-in replacement for `errors` (stdlib) and `github.com/pkg/errors` with additional features like stacktrace embedding, tagging, domains, multi-error handling, and more. + +## Features + +- **Error Creation**: Create new errors with automatic domain detection and stack traces +- **Error Wrapping**: Add context to errors while preserving the original error chain +- **Stack Traces**: Automatic stack frame capture for debugging +- **Domains**: Categorize errors by domain (package-based or custom) +- **Tags**: Attach structured key-value metadata to errors +- **Status**: Associate status information with errors +- **Multi-Error Support**: Combine multiple errors into a single error +- **Opaque Errors**: Hide internal error details from external callers +- **Secondary Errors**: Attach additional contextual errors +- **Error Reporting**: Built-in support for Sentry and custom reporters +- **Standard Library Compatible**: Works with `errors.Is`, `errors.As`, and `errors.Unwrap` + +## Installation + +```bash +go get github.com/upfluence/errors +``` + +## Quick Start + +```go +import "github.com/upfluence/errors" + +// Create a new error +err := errors.New("configuration file not found") + +// Create a formatted error +err := errors.Newf("invalid user ID: %d", userID) + +// Wrap an existing error +file, err := os.Open("config.yaml") +if err != nil { + return errors.Wrap(err, "failed to open configuration file") +} + +// Wrap with formatting +data, err := readFile(path) +if err != nil { + return errors.Wrapf(err, "failed to read file %s", path) +} +``` + +## Core Functions + +### Creating Errors + +**`New(msg string) error`** + +Creates a new error with automatic domain detection, stack trace, and opaque wrapping. + +```go +func loadConfig() error { + if !fileExists("config.yaml") { + return errors.New("configuration file not found") + } + return nil +} +``` + +**`Newf(msg string, args ...interface{}) error`** + +Creates a new formatted error with automatic domain detection, stack trace, and opaque wrapping. + +```go +func getUser(id int) error { + if id < 0 { + return errors.Newf("invalid user ID: %d", id) + } + return nil +} +``` + +### Wrapping Errors + +**`Wrap(err error, msg string) error`** + +Wraps an error with additional context and a stack frame. + +```go +file, err := os.Open("config.yaml") +if err != nil { + return errors.Wrap(err, "failed to open configuration file") +} +``` + +**`Wrapf(err error, msg string, args ...interface{}) error`** + +Wraps an error with formatted context and a stack frame. + +```go +data, err := readFile(path) +if err != nil { + return errors.Wrapf(err, "failed to read file %s", path) +} +``` + +### Error Inspection + +**`Cause(err error) error`** + +Returns the root cause of an error by recursively unwrapping it. + +```go +err := errors.New("root cause") +wrapped := errors.Wrap(err, "additional context") +root := errors.Cause(wrapped) // returns "root cause" +``` + +**`Unwrap(err error) error`** + +Returns the next error in the chain (one level). + +```go +err := errors.New("root") +wrapped := errors.Wrap(err, "wrapper") +unwrapped := errors.Unwrap(wrapped) // returns err +``` + +**`Is(err, target error) bool`** + +Reports whether any error in the chain matches the target. + +```go +if errors.Is(err, io.EOF) { + fmt.Println("End of file reached") +} +``` + +**`As(err error, target interface{}) bool`** + +Finds the first error in the chain matching the target type. + +```go +var pathErr *os.PathError +if errors.As(err, &pathErr) { + fmt.Println("Failed at path:", pathErr.Path) +} +``` + +**`IsTimeout(err error) bool`** + +Checks if any error in the chain implements `Timeout() bool` and returns true. + +```go +if errors.IsTimeout(err) { + fmt.Println("Operation timed out, retrying...") +} +``` + +**`IsOfType[T error](err error) bool`** (Go 1.18+) + +Reports whether any error in the chain matches the generic type T. This is a type-safe alternative to using `As` when you only need to check for the presence of a type without extracting it. + +```go +type MyError struct{} +func (e MyError) Error() string { return "my error" } + +err := errors.Wrap(MyError{}, "wrapped error") +if errors.IsOfType[MyError](err) { + // MyError was found in the error chain +} +``` + +**`AsType[T error](err error) (T, bool)`** (Go 1.18+) + +Attempts to convert an error to the generic type T by traversing the error chain. Returns the converted error and true if successful, or a zero value and false otherwise. This is a type-safe wrapper around `errors.As`. + +```go +type MyError struct { + Code int +} +func (e MyError) Error() string { return fmt.Sprintf("error %d", e.Code) } + +err := errors.Wrap(MyError{Code: 404}, "not found") +if myErr, ok := errors.AsType[MyError](err); ok { + fmt.Println(myErr.Code) // prints: 404 +} +``` + +## Enriching Errors + +### Stack Traces + +**`WithStack(err error) error`** + +Adds a stack frame at the current location. + +```go +err := someExternalLibrary() +if err != nil { + return errors.WithStack(err) +} +``` + +**`WithStack2[T any](v T, err error) (T, error)`** (Go 1.18+) + +Adds a stack frame at the current location, while passing through a return value. This is useful for adding stack traces to errors from functions that return multiple values (value, error) in a single line. + +If err is nil, returns (v, nil) unchanged. If err is not nil, returns (v, err_with_stack). + +```go +// Instead of this: +result, err := externalLib.DoSomething() +if err != nil { + return result, errors.WithStack(err) +} +return result, nil + +// You can write this: +return errors.WithStack2(externalLib.DoSomething()) +``` + +**`WithFrame(err error, depth int) error`** + +Adds a stack frame at a specific depth in the call stack. + +```go +func wrapError(err error) error { + return errors.WithFrame(err, 1) // Skip current frame +} +``` + +### Domains + +**`WithDomain(err error, domain string) error`** + +Attaches a domain for error categorization. + +```go +err := doSomething() +if err != nil { + return errors.WithDomain(err, "database") +} +``` + +### Tags + +**`WithTags(err error, tags map[string]interface{}) error`** + +Attaches structured metadata to an error. + +```go +err := fetchUser(userID) +if err != nil { + return errors.WithTags(err, map[string]interface{}{ + "user_id": userID, + "operation": "fetch", + }) +} +``` + +### Status + +**`WithStatus(err error, status string) error`** + +Attaches a status string to an error. + +```go +err := processRequest() +if err != nil { + return errors.WithStatus(err, "failed") +} +``` + +### Secondary Errors + +**`WithSecondaryError(err error, additionalErr error) error`** + +Attaches an additional error for context. + +```go +err := saveToDatabase(data) +if err != nil { + cacheErr := invalidateCache() + return errors.WithSecondaryError(err, cacheErr) +} +``` + +## Multi-Error Support + +**`Join(errs ...error) error`** + +Combines multiple errors (compatible with Go 1.20+ `errors.Join`). + +```go +err := errors.Join( + errors.New("first error"), + errors.New("second error"), +) +``` + +**`Combine(errs ...error) error`** + +Alternative to `Join` with the same behavior. + +```go +err := errors.Combine( + validateName(), + validateEmail(), + validateAge(), +) +``` + +**`WrapErrors(errs []error) error`** + +Combines a slice of errors. + +```go +var errs []error +if err := validateName(); err != nil { + errs = append(errs, err) +} +if err := validateEmail(); err != nil { + errs = append(errs, err) +} +if len(errs) > 0 { + return errors.WrapErrors(errs) +} +``` + +## Opaque Errors + +**`Opaque(err error) error`** + +Prevents unwrapping to hide internal implementation details. + +```go +err := doInternalOperation() +if err != nil { + return errors.Opaque(err) // Hide internal errors from callers +} +``` + +## Error Reporting + +The package includes integration with error reporting services like Sentry. + +### Reporter Interface + +```go +type Reporter interface { + io.Closer + Report(error, ReportOptions) +} +``` + +### Sentry Integration + +```go +import "github.com/upfluence/errors/reporter/sentry" + +reporter, err := sentry.NewReporter(sentry.Options{ + DSN: "your-sentry-dsn", +}) +if err != nil { + log.Fatal(err) +} +defer reporter.Close() + +reporter.Report(err, reporter.ReportOptions{ + Tags: map[string]interface{}{ + "environment": "production", + "user_id": userID, + }, +}) +``` + +### Standard Tag Keys + +The package provides standard tag keys for common error metadata: + +```go +reporter.TransactionKey // "transaction" +reporter.DomainKey // "domain" +reporter.UserEmailKey // "user.email" +reporter.UserIDKey // "user.id" +reporter.RemoteIP // "remote.ip" +reporter.HTTPRequestPathKey // "http.request.path" +reporter.HTTPRequestMethodKey // "http.request.method" +// ... and more +``` + +## Testing + +The package includes testing utilities in the `errtest` subpackage for writing error-related tests. diff --git a/cause.go b/cause.go index 6d5b847..3ffd07b 100644 --- a/cause.go +++ b/cause.go @@ -6,16 +6,26 @@ import ( "github.com/upfluence/errors/base" ) -func Cause(err error) error { return base.UnwrapAll(err) } +// Cause returns the root cause of an error by recursively unwrapping it. +func Cause(err error) error { return base.UnwrapAll(err) } + +// 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. func Unwrap(err error) error { return base.UnwrapOnce(err) } +// As finds the first error in err's chain that matches target, and if so, sets +// target to that error value and returns true. Otherwise, it returns false. func As(err error, target interface{}) bool { return errors.As(err, target) } -func Is(err, target error) bool { return errors.Is(err, target) } + +// Is reports whether any error in err's chain matches target. +func Is(err, target error) bool { return errors.Is(err, target) } type timeout interface { Timeout() bool } +// IsTimeout reports whether any error in err's chain implements the Timeout() bool +// method and returns true from that method. func IsTimeout(err error) bool { var terr timeout diff --git a/cause_go118.go b/cause_go118.go index 946eee4..a1e1cb4 100644 --- a/cause_go118.go +++ b/cause_go118.go @@ -4,6 +4,8 @@ package errors import "errors" +// IsOfType reports whether any error in err's tree matches the generic type T. +// It traverses the error chain using Unwrap until it finds a match or reaches the end. func IsOfType[T error](err error) bool { for { if _, ok := err.(T); ok || err == nil { @@ -14,6 +16,9 @@ func IsOfType[T error](err error) bool { } } +// AsType attempts to convert err to the generic type T by traversing the error chain. +// It returns the converted error and true if successful, or a zero value and false otherwise. +// This is a type-safe wrapper around errors.As. func AsType[T error](err error) (T, bool) { var e T diff --git a/domain.go b/domain.go index 74b0641..e18f0ac 100644 --- a/domain.go +++ b/domain.go @@ -2,6 +2,7 @@ package errors import "github.com/upfluence/errors/domain" +// WithDomain attaches a domain string to the error for categorization purposes. func WithDomain(err error, d string) error { return domain.WithDomain(err, domain.Domain(d)) } diff --git a/domain/domain.go b/domain/domain.go index 1b06ce7..a36d05f 100644 --- a/domain/domain.go +++ b/domain/domain.go @@ -1,3 +1,9 @@ +// Package domain provides error domain classification and extraction. +// +// A domain identifies the package or module where an error originated, +// which is useful for categorizing and routing errors in large applications. +// The domain is automatically extracted from the call stack and can be +// attached to errors for later retrieval. package domain import ( @@ -5,20 +11,28 @@ import ( "github.com/upfluence/errors/stacktrace" ) +// NoDomain is returned when an error has no associated domain. const NoDomain = Domain("unknown") +// Domain represents the package or module where an error originated. type Domain string +// PackageDomain returns the domain for the calling package. func PackageDomain() Domain { return PackageDomainAtDepth(1) } +// PackageDomainAtDepth returns the domain for the package at the specified +// call stack depth. depth=0 returns the caller's package, depth=1 returns +// the caller's caller's package, etc. func PackageDomainAtDepth(depth int) Domain { var fn, _, _ = stacktrace.Caller(1 + depth).Location() return Domain(stacktrace.PackageName(fn)) } +// GetDomain extracts the domain from an error by traversing the error chain. +// Returns NoDomain if no domain is found. func GetDomain(err error) Domain { for { if wd, ok := err.(interface{ Domain() Domain }); ok { diff --git a/errtest/assertion.go b/errtest/assertion.go index 736a370..2bab35b 100644 --- a/errtest/assertion.go +++ b/errtest/assertion.go @@ -1,3 +1,8 @@ +// Package errtest provides testing utilities for error assertions. +// +// This package offers a fluent interface for asserting error conditions in tests, +// including checking for nil errors, error equality, and error causes. +// It integrates with testify/assert for consistent test failure reporting. package errtest import ( @@ -9,6 +14,7 @@ import ( "github.com/upfluence/errors" ) +// ErrorAssertion represents an assertion to be performed on an error. type ErrorAssertion interface { Assert(testing.TB, error) } @@ -21,6 +27,8 @@ func (eas multiErrorAssertion) Assert(t testing.TB, err error) { } } +// CombineErrorAssertion combines multiple ErrorAssertion instances into one. +// All assertions will be executed when Assert is called. func CombineErrorAssertion(eas ...ErrorAssertion) ErrorAssertion { if len(eas) == 1 { return eas[0] @@ -31,14 +39,18 @@ func CombineErrorAssertion(eas ...ErrorAssertion) ErrorAssertion { type noErrorAssertion struct{} +// NoErrorAssertion is an ErrorAssertion that performs no checks. +// Useful as a placeholder or default value. var NoErrorAssertion ErrorAssertion = noErrorAssertion{} func (noErrorAssertion) Assert(testing.TB, error) {} +// ErrorAssertionFunc is a function adapter for the ErrorAssertion interface. type ErrorAssertionFunc func(testing.TB, error) func (fn ErrorAssertionFunc) Assert(t testing.TB, err error) { fn(t, err) } +// NoError creates an ErrorAssertion that asserts the error is nil. func NoError(msgAndArgs ...interface{}) ErrorAssertion { return ErrorAssertionFunc( func(t testing.TB, err error) { @@ -51,12 +63,15 @@ func NoError(msgAndArgs ...interface{}) ErrorAssertion { ) } +// ErrorEqual creates an ErrorAssertion that asserts the error equals the expected error. func ErrorEqual(want error, msgAndArgs ...interface{}) ErrorAssertion { return ErrorAssertionFunc( func(t testing.TB, out error) { assert.Equal(t, want, out, msgAndArgs...) }, ) } +// ErrorCause creates an ErrorAssertion that asserts the error's root cause +// equals the expected error. func ErrorCause(want error, msgAndArgs ...interface{}) ErrorAssertion { return ErrorAssertionFunc( func(t testing.TB, out error) { diff --git a/example_cause_go118_test.go b/example_cause_go118_test.go new file mode 100644 index 0000000..5d6fbfa --- /dev/null +++ b/example_cause_go118_test.go @@ -0,0 +1,42 @@ +//go:build go1.18 + +package errors_test + +import ( + "fmt" + + "github.com/upfluence/errors" +) + +type MyError struct { + message string +} + +func (e MyError) Error() string { return e.message } + +type MyErrorWithCode struct { + Code int + Message string +} + +func (e MyErrorWithCode) Error() string { + return fmt.Sprintf("error %d: %s", e.Code, e.Message) +} + +func ExampleIsOfType() { + err := errors.Wrap(MyError{message: "my error"}, "wrapped error") + + if errors.IsOfType[MyError](err) { + fmt.Println("MyError was found in the error chain") + } + // Output: MyError was found in the error chain +} + +func ExampleAsType() { + err := errors.Wrap(MyErrorWithCode{Code: 404, Message: "not found"}, "request failed") + + if myErr, ok := errors.AsType[MyErrorWithCode](err); ok { + fmt.Println(myErr.Code) + } + // Output: 404 +} diff --git a/example_cause_test.go b/example_cause_test.go new file mode 100644 index 0000000..1e9081a --- /dev/null +++ b/example_cause_test.go @@ -0,0 +1,59 @@ +package errors_test + +import ( + "fmt" + "io" + "net" + "os" + + "github.com/upfluence/errors" +) + +func ExampleCause() { + rootErr := errors.New("root cause") + wrapped := errors.Wrap(rootErr, "additional context") + root := errors.Cause(wrapped) + fmt.Println(root) + // Output: root cause +} + +func ExampleUnwrap() { + rootErr := errors.New("root") + wrapped := errors.Wrap(rootErr, "wrapper") + unwrapped := errors.Unwrap(wrapped) + fmt.Println(unwrapped) + // Output: wrapper: root +} + +func ExampleAs() { + // Create a PathError + pathErr := &os.PathError{Op: "open", Path: "/tmp/file.txt", Err: os.ErrNotExist} + wrapped := errors.Wrap(pathErr, "failed to process file") + + // Use As to extract the PathError + var targetErr *os.PathError + if errors.As(wrapped, &targetErr) { + fmt.Println("Failed at path:", targetErr.Path) + } + // Output: Failed at path: /tmp/file.txt +} + +func ExampleIs() { + err := errors.Wrap(io.EOF, "read operation failed") + + if errors.Is(err, io.EOF) { + fmt.Println("End of file reached") + } + // Output: End of file reached +} + +func ExampleIsTimeout() { + // Create a timeout error + timeoutErr := &net.DNSError{IsTimeout: true} + wrapped := errors.Wrap(timeoutErr, "network operation failed") + + if errors.IsTimeout(wrapped) { + fmt.Println("Operation timed out, retrying...") + } + // Output: Operation timed out, retrying... +} diff --git a/example_domain_test.go b/example_domain_test.go new file mode 100644 index 0000000..784b00c --- /dev/null +++ b/example_domain_test.go @@ -0,0 +1,16 @@ +package errors_test + +import ( + "fmt" + + "github.com/upfluence/errors" +) + +func ExampleWithDomain() { + // Create an error and attach a domain for categorization + err := errors.New("operation failed") + errWithDomain := errors.WithDomain(err, "database") + + fmt.Println(errWithDomain) + // Output: operation failed +} diff --git a/example_multi_test.go b/example_multi_test.go new file mode 100644 index 0000000..52fcc8c --- /dev/null +++ b/example_multi_test.go @@ -0,0 +1,40 @@ +package errors_test + +import ( + "fmt" + + "github.com/upfluence/errors" +) + +func ExampleWrapErrors() { + var errs []error + + // Simulate validation errors + errs = append(errs, errors.New("name is required")) + errs = append(errs, errors.New("email is invalid")) + + if len(errs) > 0 { + err := errors.WrapErrors(errs) + fmt.Println(err) + } + // Output: [name is required, email is invalid] +} + +func ExampleCombine() { + err := errors.Combine( + errors.New("validation failed: name"), + errors.New("validation failed: email"), + errors.New("validation failed: age"), + ) + fmt.Println(err) + // Output: [validation failed: name, validation failed: email, validation failed: age] +} + +func ExampleJoin() { + err := errors.Join( + errors.New("first error"), + errors.New("second error"), + ) + fmt.Println(err) + // Output: [first error, second error] +} diff --git a/example_opaque_test.go b/example_opaque_test.go new file mode 100644 index 0000000..ce1ef0a --- /dev/null +++ b/example_opaque_test.go @@ -0,0 +1,23 @@ +package errors_test + +import ( + "fmt" + + "github.com/upfluence/errors" +) + +func ExampleOpaque() { + // Create an internal error that we want to hide + internalErr := errors.New("database connection pool exhausted") + + // Make it opaque to prevent callers from inspecting internal details + err := errors.Opaque(internalErr) + + // The error message is still available + fmt.Println(err) + + // But unwrapping won't work - it returns nil + fmt.Println(errors.Unwrap(err) == nil) + // Output: database connection pool exhausted + // true +} diff --git a/example_secondary_test.go b/example_secondary_test.go new file mode 100644 index 0000000..7acee00 --- /dev/null +++ b/example_secondary_test.go @@ -0,0 +1,21 @@ +package errors_test + +import ( + "fmt" + + "github.com/upfluence/errors" +) + +func ExampleWithSecondaryError() { + // Simulate a primary error + dbErr := errors.New("failed to save data to database") + + // Simulate a secondary error that occurred while handling the primary error + cacheErr := errors.New("failed to invalidate cache") + + // Attach the secondary error for additional context + err := errors.WithSecondaryError(dbErr, cacheErr) + + fmt.Println(err) + // Output: failed to save data to database [ with secondary error: failed to invalidate cache] +} diff --git a/example_stacktrace_test.go b/example_stacktrace_test.go new file mode 100644 index 0000000..9994d36 --- /dev/null +++ b/example_stacktrace_test.go @@ -0,0 +1,63 @@ +package errors_test + +import ( + "fmt" + + "github.com/upfluence/errors" +) + +func ExampleWithStack() { + // Simulate an error from an external library + externalErr := fmt.Errorf("external library error") + + // Add stack trace at current location + err := errors.WithStack(externalErr) + + fmt.Println(err) + // Output: external library error +} + +// Result represents a simple computation result +type Result struct { + Value int +} + +// mockCompute simulates an external library function +func mockCompute(input int) (Result, error) { + if input < 0 { + return Result{}, fmt.Errorf("negative input not allowed") + } + return Result{Value: input * 2}, nil +} + +func ExampleWithStack2() { + // Demonstrates using WithStack2 to add stack traces inline while returning values + compute := func(input int) (Result, error) { + // WithStack2 adds stack trace in a single line + return errors.WithStack2(mockCompute(input)) + } + + // Success case - error is nil + result, err := compute(5) + fmt.Printf("Result: %+v, Error: %v\n", result, err) + + // Error case - error gets stack trace added + result, err = compute(-1) + fmt.Printf("Result: %+v, Error: %v\n", result, err) + + // Output: + // Result: {Value:10}, Error: + // Result: {Value:0}, Error: negative input not allowed +} + +func ExampleWithFrame() { + // This helper function wraps errors with the caller's location + wrapError := func(err error) error { + // Skip 1 frame to capture the caller's location instead of this function + return errors.WithFrame(err, 1) + } + + err := wrapError(fmt.Errorf("original error")) + fmt.Println(err) + // Output: original error +} diff --git a/example_stats_test.go b/example_stats_test.go new file mode 100644 index 0000000..26075c3 --- /dev/null +++ b/example_stats_test.go @@ -0,0 +1,16 @@ +package errors_test + +import ( + "fmt" + + "github.com/upfluence/errors" +) + +func ExampleWithStatus() { + // Create an error and attach a status + err := errors.New("request processing failed") + errWithStatus := errors.WithStatus(err, "failed") + + fmt.Println(errWithStatus) + // Output: request processing failed +} diff --git a/example_tags_test.go b/example_tags_test.go new file mode 100644 index 0000000..b74b692 --- /dev/null +++ b/example_tags_test.go @@ -0,0 +1,21 @@ +package errors_test + +import ( + "fmt" + + "github.com/upfluence/errors" +) + +func ExampleWithTags() { + userID := "user123" + + // Create an error and attach tags for additional context + err := errors.New("failed to fetch user") + errWithTags := errors.WithTags(err, map[string]interface{}{ + "user_id": userID, + "operation": "fetch", + }) + + fmt.Println(errWithTags) + // Output: failed to fetch user +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..fd14afe --- /dev/null +++ b/example_test.go @@ -0,0 +1,38 @@ +package errors_test + +import ( + "fmt" + "os" + + "github.com/upfluence/errors" +) + +func ExampleNew() { + err := errors.New("configuration file not found") + fmt.Println(err) + // Output: configuration file not found +} + +func ExampleNewf() { + id := -5 + err := errors.Newf("invalid user ID: %d", id) + fmt.Println(err) + // Output: invalid user ID: -5 +} + +func ExampleWrap() { + // Simulate an error from os.Open + baseErr := &os.PathError{Op: "open", Path: "config.yaml", Err: os.ErrNotExist} + err := errors.Wrap(baseErr, "failed to open configuration file") + fmt.Println(err) + // Output: failed to open configuration file: open config.yaml: file does not exist +} + +func ExampleWrapf() { + // Simulate an error from reading a file + path := "/tmp/data.txt" + baseErr := &os.PathError{Op: "read", Path: path, Err: os.ErrPermission} + err := errors.Wrapf(baseErr, "failed to read file %s", path) + fmt.Println(err) + // Output: failed to read file /tmp/data.txt: read /tmp/data.txt: permission denied +} diff --git a/message/with_message.go b/message/with_message.go index 5ce75c8..ac7a826 100644 --- a/message/with_message.go +++ b/message/with_message.go @@ -1,3 +1,8 @@ +// Package message provides error wrapping with additional context messages. +// +// This package allows you to wrap errors with descriptive messages while +// preserving the original error. The messages are formatted and prepended +// to the original error message, creating a clear error chain. package message import "fmt" @@ -23,6 +28,8 @@ func (wm *withMessage) Unwrap() error { return wm.cause } func (wm *withMessage) Cause() error { return wm.cause } func (wm *withMessage) Args() []interface{} { return wm.args } +// WithMessage wraps an error with an additional context message. +// Returns nil if err is nil. func WithMessage(err error, msg string) error { if err == nil { return nil @@ -31,6 +38,8 @@ func WithMessage(err error, msg string) error { return &withMessage{cause: err, fmt: msg} } +// WithMessagef wraps an error with a formatted context message. +// Returns nil if err is nil. func WithMessagef(err error, msg string, args ...interface{}) error { if err == nil { return nil diff --git a/multi.go b/multi.go index 0e241b9..ca93fb7 100644 --- a/multi.go +++ b/multi.go @@ -2,6 +2,12 @@ package errors import "github.com/upfluence/errors/multi" +// WrapErrors combines multiple errors into a single error and adds a stack frame. func WrapErrors(errs []error) error { return WithFrame(multi.Wrap(errs), 1) } -func Combine(errs ...error) error { return WithFrame(multi.Wrap(errs), 1) } -func Join(errs ...error) error { return WithFrame(multi.Wrap(errs), 1) } + +// Combine combines multiple errors into a single error and adds a stack frame. +func Combine(errs ...error) error { return WithFrame(multi.Wrap(errs), 1) } + +// Join combines multiple errors into a single error and adds a stack frame. +// This function is compatible with the errors.Join function introduced in Go 1.20. +func Join(errs ...error) error { return WithFrame(multi.Wrap(errs), 1) } diff --git a/multi/multi_error.go b/multi/multi_error.go index 9807179..39120d4 100644 --- a/multi/multi_error.go +++ b/multi/multi_error.go @@ -1,3 +1,9 @@ +// Package multi provides support for handling multiple errors as a single error. +// +// This package allows combining multiple errors into one, which is useful for +// operations that can fail in multiple ways or when collecting errors from +// concurrent operations. It automatically flattens nested multi-errors and +// filters out nil errors. package multi import ( @@ -51,6 +57,10 @@ func (errs multiError) Tags() map[string]interface{} { return allTags } +// Wrap combines multiple errors into a single error. +// Returns nil if all errors are nil, returns the single error if only one is non-nil, +// or returns a multiError containing all non-nil errors. +// Automatically flattens nested multiErrors. func Wrap(errs []error) error { switch len(errs) { case 0: @@ -79,12 +89,17 @@ func Wrap(errs []error) error { } } +// Combine combines multiple errors into a single error. +// This is an alias for Wrap that accepts variadic arguments. func Combine(errs ...error) error { return Wrap(errs) } +// MultiError is an interface for errors that contain multiple errors. type MultiError interface { Errors() []error } +// ExtractErrors recursively extracts all errors from an error, +// flattening any MultiError instances. Returns nil if err is nil. func ExtractErrors(err error) []error { if err == nil { return nil diff --git a/opaque.go b/opaque.go index e52c1f5..c62efbd 100644 --- a/opaque.go +++ b/opaque.go @@ -2,4 +2,5 @@ package errors import "github.com/upfluence/errors/opaque" +// Opaque wraps an error to prevent unwrapping, hiding the underlying error chain. func Opaque(err error) error { return opaque.Opaque(err) } diff --git a/opaque/opaque_error.go b/opaque/opaque_error.go index 9f3e1f9..f888634 100644 --- a/opaque/opaque_error.go +++ b/opaque/opaque_error.go @@ -1,3 +1,10 @@ +// Package opaque provides a way to create opaque errors that hide the underlying +// error type while preserving metadata. +// +// Opaque errors prevent callers from using errors.Is or errors.As to match +// against the wrapped error, while still exposing useful metadata like domain, +// tags, and stacktrace. This is useful for API boundaries where you want to +// expose error information without leaking implementation details. package opaque import ( @@ -24,6 +31,8 @@ func (oe *opaqueError) Frames() []stacktrace.Frame { return stacktrace.GetFrames(oe.cause) } +// Opaque wraps an error to make it opaque, preventing type assertions +// while preserving metadata like domain, tags, and stacktrace. func Opaque(err error) error { return &opaqueError{cause: err} } diff --git a/recovery/runtime_error.go b/recovery/runtime_error.go index dc4456f..60206a5 100644 --- a/recovery/runtime_error.go +++ b/recovery/runtime_error.go @@ -1,3 +1,8 @@ +// Package recovery provides utilities for converting panic recovery values into errors. +// +// This package helps convert the values returned by recover() into proper error types. +// It handles different value types intelligently, preserving runtime.Error instances +// and converting other types into descriptive error messages. package recovery import ( @@ -24,6 +29,10 @@ func (re *runtimeError) Error() string { return fmt.Sprintf("%T: %v", re.v, re.v) } +// WrapRecoverResult converts a panic recovery value into an error. +// Returns nil if v is nil. +// Preserves runtime.Error instances as-is. +// Converts other values into errors with descriptive messages. func WrapRecoverResult(v interface{}) error { if v == nil { return nil diff --git a/reporter/inhibit/reporter.go b/reporter/inhibit/reporter.go index 25fc266..9bf3fbb 100644 --- a/reporter/inhibit/reporter.go +++ b/reporter/inhibit/reporter.go @@ -1,3 +1,8 @@ +// Package inhibit provides a reporter wrapper that can selectively suppress error reports. +// +// This package allows you to wrap any reporter.Reporter with error inhibition logic. +// Error inhibitors can inspect errors and decide whether they should be reported, +// which is useful for filtering out known errors, noise, or errors below certain thresholds. package inhibit import ( @@ -6,14 +11,17 @@ import ( "github.com/upfluence/errors/reporter" ) +// ErrorInhibitor determines whether an error should be inhibited from reporting. type ErrorInhibitor interface { Inhibit(error) bool } +// ErrorInhibitorFunc is a function adapter for the ErrorInhibitor interface. type ErrorInhibitorFunc func(error) bool func (fn ErrorInhibitorFunc) Inhibit(err error) bool { return fn(err) } +// Reporter wraps another reporter with error inhibition logic. type Reporter struct { r reporter.Reporter @@ -21,10 +29,13 @@ type Reporter struct { eis []ErrorInhibitor } +// NewReporter creates a new inhibit reporter wrapping the given reporter +// with the specified error inhibitors. func NewReporter(r reporter.Reporter, eis ...ErrorInhibitor) *Reporter { return &Reporter{r: r, eis: eis} } +// AddErrorInhibitors adds additional error inhibitors to the reporter. func (r *Reporter) AddErrorInhibitors(eis ...ErrorInhibitor) { r.mu.Lock() defer r.mu.Unlock() @@ -32,8 +43,10 @@ func (r *Reporter) AddErrorInhibitors(eis ...ErrorInhibitor) { r.eis = append(r.eis, eis...) } +// Close closes the underlying reporter. func (r *Reporter) Close() error { return r.r.Close() } +// Report reports an error if it is not inhibited by any error inhibitors. func (r *Reporter) Report(err error, opts reporter.ReportOptions) { r.mu.RLock() diff --git a/reporter/reporter.go b/reporter/reporter.go index ab5af48..b77d806 100644 --- a/reporter/reporter.go +++ b/reporter/reporter.go @@ -1,7 +1,13 @@ +// Package reporter provides an interface for error reporting to external services. +// +// This package defines the Reporter interface and common tag keys for structured +// error reporting. Reporters can send errors to services like Sentry with rich +// contextual information including request details, user information, and custom tags. package reporter import "io" +// Common tag keys for error reporting metadata. const ( TransactionKey = "transaction" DomainKey = "domain" @@ -28,14 +34,17 @@ const ( ThriftRequestBodyKey = "thrift.request.body" ) +// NopReporter is a Reporter implementation that does nothing. var NopReporter Reporter = nopReporter{} +// ReportOptions contains options for reporting an error. type ReportOptions struct { Tags map[string]interface{} Depth int } +// Reporter is the interface for error reporting implementations. type Reporter interface { io.Closer diff --git a/reporter/sentry/reporter.go b/reporter/sentry/reporter.go index 4f286f3..edc8579 100644 --- a/reporter/sentry/reporter.go +++ b/reporter/sentry/reporter.go @@ -1,3 +1,9 @@ +// Package sentry provides a Sentry implementation of the reporter.Reporter interface. +// +// This package integrates with Sentry for error tracking and monitoring. +// It automatically extracts error metadata including stacktraces, tags, domains, +// and request information, formatting them for Sentry ingestion. The reporter +// supports tag whitelisting/blacklisting for controlling which metadata is sent. package sentry import ( @@ -18,6 +24,7 @@ import ( "github.com/upfluence/errors/tags" ) +// Reporter is a Sentry error reporter implementation. type Reporter struct { cl *sentry.Client @@ -27,6 +34,7 @@ type Reporter struct { timeout time.Duration } +// NewReporter creates a new Sentry reporter with the given options. func NewReporter(os ...Option) (*Reporter, error) { var opts = defaultOptions @@ -52,10 +60,14 @@ func NewReporter(os ...Option) (*Reporter, error) { timeout: opts.Timeout, }, nil } + +// WhitelistTag adds tag whitelist functions that determine which tags +// should be included as Sentry tags (vs extra data). func (r *Reporter) WhitelistTag(fns ...func(string) bool) { r.tagWhitelist = append(r.tagWhitelist, fns...) } +// Report sends an error to Sentry with the given options. func (r *Reporter) Report(err error, opts reporter.ReportOptions) { evt := r.buildEvent(err, opts) @@ -66,6 +78,7 @@ func (r *Reporter) Report(err error, opts reporter.ReportOptions) { r.cl.CaptureEvent(evt, nil, nil) } +// Close flushes pending events to Sentry and releases resources. func (r *Reporter) Close() error { r.cl.Flush(r.timeout) return nil diff --git a/secondary.go b/secondary.go index cdccb57..8c1f1dd 100644 --- a/secondary.go +++ b/secondary.go @@ -2,6 +2,7 @@ package errors import "github.com/upfluence/errors/secondary" +// WithSecondaryError attaches an additional error to the primary error for context. func WithSecondaryError(err error, additionalErr error) error { return secondary.WithSecondaryError(err, additionalErr) } diff --git a/secondary/with_secondary.go b/secondary/with_secondary.go index a0a4c2d..b725227 100644 --- a/secondary/with_secondary.go +++ b/secondary/with_secondary.go @@ -1,3 +1,8 @@ +// Package secondary provides support for attaching secondary errors to a primary error. +// +// This package is useful when an operation fails and a cleanup or recovery operation +// also fails. The secondary error's tags are included in the combined error, allowing +// additional context to be preserved without losing the primary error information. package secondary import ( @@ -33,6 +38,9 @@ func (ws *withSecondary) Errors() []error { return []error{ws.cause, ws.second} } +// WithSecondaryError combines a primary error with a secondary error. +// Returns additionalErr if err is nil, returns err if additionalErr is nil, +// or returns a combined error containing both. func WithSecondaryError(err error, additionalErr error) error { if err == nil { return additionalErr diff --git a/stacktrace.go b/stacktrace.go index c8167ae..d9c1b5d 100644 --- a/stacktrace.go +++ b/stacktrace.go @@ -2,10 +2,35 @@ package errors import "github.com/upfluence/errors/stacktrace" +// WithStack wraps an error with a stack frame captured at the call site. func WithStack(err error) error { return WithFrame(err, 1) } +// WithStack2 wraps an error with a stack frame captured at the call site, while +// passing through a return value. This is useful for adding stack traces to errors +// from functions that return multiple values (value, error). +// +// If err is nil, returns (v, nil) unchanged. If err is not nil, returns (v, err_with_stack) +// where err_with_stack includes the stack frame. +// +// Example: +// +// return errors.WithStack2(externalLib.DoSomething()) +// +// This is equivalent to but more concise than: +// +// result, err := externalLib.DoSomething() +// if err != nil { +// return result, errors.WithStack(err) +// } +// return result, nil +func WithStack2[T any](v T, err error) (T, error) { + return v, WithFrame(err, 1) +} + +// WithFrame wraps an error with a stack frame at the specified depth in the call stack. +// The depth parameter indicates how many stack frames to skip (0 = current frame). func WithFrame(err error, d int) error { return stacktrace.WithFrame(err, d+1) } diff --git a/stacktrace/stacktrace.go b/stacktrace/stacktrace.go index d7093e0..05aa2cd 100644 --- a/stacktrace/stacktrace.go +++ b/stacktrace/stacktrace.go @@ -1,3 +1,9 @@ +// Package stacktrace provides utilities for capturing and managing stack traces. +// +// This package allows capturing stack frame information at error creation time, +// which can be attached to errors and later extracted for debugging or reporting. +// It supports both single frame and full stacktrace capture, and provides utilities +// for extracting package names from function names. package stacktrace import ( @@ -7,8 +13,11 @@ import ( "github.com/upfluence/errors/base" ) +// Frame represents a single program counter location in a stack trace. type Frame uintptr +// Caller captures a single stack frame at the specified depth. +// depth=0 returns the caller's frame, depth=1 returns the caller's caller's frame, etc. func Caller(depth int) Frame { var callers [1]uintptr @@ -17,6 +26,8 @@ func Caller(depth int) Frame { return Frame(callers[0]) } +// Stacktrace captures multiple stack frames starting at the specified depth. +// Returns up to count frames from the call stack. func Stacktrace(depth, count int) []Frame { var ( callers = make([]uintptr, count) @@ -32,12 +43,14 @@ func Stacktrace(depth, count int) []Frame { return fs } +// Location returns the function name, file path, and line number for this frame. func (f Frame) Location() (string, string, int) { fr, _ := runtime.CallersFrames([]uintptr{uintptr(f)}).Next() return fr.Function, fr.File, fr.Line } +// GetFrames extracts all stack frames from an error by traversing the error chain. func GetFrames(err error) []Frame { var fs []Frame @@ -59,6 +72,8 @@ func GetFrames(err error) []Frame { return fs } +// PackageName extracts the package path from a fully qualified function name. +// Returns an empty string for compiler-generated symbols. func PackageName(name string) string { // A prefix of "type." and "go." is a compiler-generated symbol that doesn't belong to any package. // See variable reservedimports in cmd/compile/internal/gc/subr.go diff --git a/stats.go b/stats.go index bdf6082..8ce6722 100644 --- a/stats.go +++ b/stats.go @@ -2,6 +2,7 @@ package errors import "github.com/upfluence/errors/stats" +// WithStatus attaches a status string to the error and adds a stack frame. func WithStatus(err error, status string) error { return WithFrame(stats.WithStatus(err, status), 1) } diff --git a/stats/statuser.go b/stats/statuser.go index 627bc32..caeb0ae 100644 --- a/stats/statuser.go +++ b/stats/statuser.go @@ -1,3 +1,8 @@ +// Package stats provides utilities for extracting status strings from errors. +// +// This package allows errors to expose a status string that can be used for +// metrics, logging, or categorization. It traverses the error chain to find +// status information and provides fallback mechanisms when no status is found. package stats import ( @@ -6,10 +11,12 @@ import ( "github.com/upfluence/errors/base" ) +// Statuser provides a custom status string for an error. type Statuser interface { Status(error) string } +// ExtractStatusOption configures status extraction behavior. type ExtractStatusOption func(*extractStatusOptions) type extractStatusOptions struct { @@ -33,6 +40,10 @@ func (defaultStatuser) Status(err error) string { } } +// GetStatus extracts a status string from an error. +// Returns the success status string if err is nil. +// Traverses the error chain looking for a Status() method, +// falling back to the configured Statuser if none is found. func GetStatus(err error, opts ...ExtractStatusOption) string { type statuser interface { Status() string diff --git a/tags.go b/tags.go index c5c2b7c..09bb756 100644 --- a/tags.go +++ b/tags.go @@ -2,6 +2,7 @@ package errors import "github.com/upfluence/errors/tags" +// WithTags attaches key-value tags to the error for additional context and adds a stack frame. func WithTags(err error, vs map[string]interface{}) error { return WithFrame(tags.WithTags(err, vs), 1) } diff --git a/tags/tags.go b/tags/tags.go index bcafc19..380d367 100644 --- a/tags/tags.go +++ b/tags/tags.go @@ -1,7 +1,16 @@ +// Package tags provides utilities for extracting tags from errors. +// +// This package allows errors to carry arbitrary key-value metadata (tags) +// that can be used for logging, metrics, or error reporting. It traverses +// the error chain to collect all tags, with outer tags taking precedence +// over inner tags when keys conflict. package tags import "github.com/upfluence/errors/base" +// GetTags extracts all tags from an error by traversing the error chain. +// Returns nil if no tags are found. +// When multiple errors in the chain have the same tag key, the outermost value is used. func GetTags(err error) map[string]interface{} { var tags map[string]interface{} diff --git a/wrap.go b/wrap.go index 738963b..8fe1384 100644 --- a/wrap.go +++ b/wrap.go @@ -9,6 +9,8 @@ import ( "github.com/upfluence/errors/opaque" ) +// New creates a new error with the given message. The error is wrapped with a +// stack frame, domain derived from the calling package, and made opaque. func New(msg string) error { return opaque.Opaque( domain.WithDomain( @@ -18,6 +20,8 @@ func New(msg string) error { ) } +// Newf creates a new error with a formatted message. The error is wrapped with a +// stack frame, domain derived from the calling package, and made opaque. func Newf(msg string, args ...interface{}) error { return opaque.Opaque( domain.WithDomain( @@ -27,10 +31,12 @@ func Newf(msg string, args ...interface{}) error { ) } +// Wrap wraps an error with an additional message and stack frame. func Wrap(err error, msg string) error { return WithFrame(message.WithMessage(err, msg), 1) } +// Wrapf wraps an error with a formatted message and stack frame. func Wrapf(err error, msg string, args ...interface{}) error { return WithFrame(message.WithMessagef(err, msg, args...), 1) }