From 18a40ea41890c64cf7cb8ec03110d6c5f8b95905 Mon Sep 17 00:00:00 2001 From: arisnguyenit97 Date: Mon, 9 Dec 2024 23:34:18 +0700 Subject: [PATCH] :mute: silent changes: add base functions wrap errors #4 --- test/wraperr_test.go | 1 + types.go | 101 +++++++++++++++-- wraperr.go | 250 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 test/wraperr_test.go diff --git a/test/wraperr_test.go b/test/wraperr_test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/test/wraperr_test.go @@ -0,0 +1 @@ +package test diff --git a/types.go b/types.go index c83d9fb..5ccec3f 100644 --- a/types.go +++ b/types.go @@ -43,18 +43,101 @@ type wrapper struct { } // Frame represents a program counter inside a stack frame. -// For historical reasons if Frame is interpreted as a uintptr -// its value represents the program counter + 1. +// A `Frame` is essentially a single point in the stack trace, +// representing a program counter (the location in code) at the +// time of a function call. Historically, for compatibility reasons, +// a `Frame` is interpreted as a `uintptr`, but the value stored +// in the `Frame` represents the program counter + 1. This allows +// for distinguishing between an invalid program counter and a valid one. +// +// A `Frame` is typically used within a `StackTrace` to track the +// sequence of function calls leading to the current point in the program. +// A frame is a low-level representation of a specific place in the code, +// helping in debugging by pinpointing the exact line of execution that caused +// an error or event. +// +// Example usage: +// +// var f Frame = Frame(0x1234567890) +// fmt.Println(f) // Prints the value of the program counter + 1 type Frame uintptr -// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +// StackTrace represents a stack of Frames, which are ordered from the +// innermost (newest) function call to the outermost (oldest) function call +// in the current stack. It provides a high-level representation of the +// sequence of function calls (the call stack) that led to the current execution +// point, typically used for debugging or error reporting. The `StackTrace` +// contains a slice of `Frame` values, which can be interpreted as program +// counters in the call stack. +// +// The `StackTrace` can be used to generate detailed stack traces when an +// error occurs, helping developers track down the sequence of function +// calls that resulted in the error. For example, it may be used in conjunction +// with the `underlying` and `underlyingStack` types to record where an error +// occurred in the code (using `Callers()` to populate the stack) and provide +// information on the call path leading to the error. +// +// Example usage: +// +// var trace StackTrace = StackTrace{Frame(0x1234567890), Frame(0x0987654321)} +// fmt.Println(trace) // Prints the stack trace with the frames type StackTrace []Frame -// stack represents a stack of program counters. +// stack represents a stack of program counters. It is a slice of `uintptr` +// values that store the addresses (or locations) of program counters +// from a function call stack. This is useful for tracking where errors +// or events originated in the code, and it is used by the other error +// types to capture a stack trace at the time of error creation. +// +// The stack is typically populated using a function like `Callers()`, +// which retrieves the current call stack. type stack []uintptr -// fundamental is an error that has a message and a stack, but no caller. -// type fundamental struct { -// msg string -// *stack -// } +// underlying represents an error with an associated message and a stack trace. +// It is used to encapsulate the message (i.e., what went wrong) and the +// program's call stack (i.e., where the error occurred) at the point when +// the error is created. This type is used internally by functions like +// `WithError` and `WithErrorf`. +// +// Fields: +// - msg: The error message describing what went wrong. This is a string +// representation of the error context. +// - stack: A pointer to a stack of program counters that represent the +// stack trace where the error was created. +type underlying struct { + msg string // Message that describes the error + *stack // Pointer to the stack trace where the error occurred +} + +// underlyingStack is an error type that contains an existing error and its +// associated stack trace. It is used when we need to annotate an existing +// error with a stack trace, preserving the original error while adding +// additional information such as the place where the annotation occurred. +// +// This type is typically used in functions like `Wrap` and `Wrapf`. +// +// Fields: +// - error: The original error that is being wrapped or annotated. +// - stack: A pointer to the stack trace captured at the point this +// annotation was added. +type underlyingStack struct { + error // The original error being wrapped or annotated + *stack // Pointer to the stack trace where the annotation occurred +} + +// underlyingMessage represents an error with a cause (another error) +// and a message. This type is used to create errors that have both +// an associated message and a "cause," which can be another error +// that led to the current one. It is commonly used to propagate errors +// and add context to the original error message. +// +// Fields: +// - cause: The underlying error that caused this error. This could be +// another error returned from a function, allowing us to trace back +// the error chain. +// - msg: A string message describing the context of the error, which +// can be appended to the original cause message to provide more clarity. +type underlyingMessage struct { + cause error // The original error being wrapped or annotated + msg string // The message describing the additional context for the error +} diff --git a/wraperr.go b/wraperr.go index aa7da50..21c7949 100644 --- a/wraperr.go +++ b/wraperr.go @@ -1 +1,251 @@ package wrapify + +import ( + "fmt" + "io" +) + +// WithError returns an error with the supplied message and records the stack trace +// at the point it was called. The error contains the message and the stack trace +// which can be used for debugging or logging the error along with the call stack. +// +// Usage example: +// +// err := WithError("Something went wrong") +// fmt.Println(err) // "Something went wrong" along with stack trace +func WithError(message string) error { + return &underlying{ + msg: message, + stack: Callers(), + } +} + +// WithErrorf formats the given arguments according to the format specifier and +// returns the formatted string as an error. It also records the stack trace at +// the point it was called. +// +// Usage example: +// +// err := WithErrorf("Failed to load file %s", filename) +// fmt.Println(err) // "Failed to load file " along with stack trace +func WithErrorf(format string, args ...interface{}) error { + return &underlying{ + msg: fmt.Sprintf(format, args...), + stack: Callers(), + } +} + +// WithErrStack annotates an existing error with a stack trace at the point +// WithErrStack was called. If the provided error is nil, it simply returns nil. +// +// Usage example: +// +// err := errors.New("original error") +// errWithStack := WithErrStack(err) +// fmt.Println(errWithStack) // original error with stack trace +func WithErrStack(err error) error { + if err == nil { + return nil + } + return &underlyingStack{ + err, + Callers(), + } +} + +// Wrap returns an error that annotates the provided error with a new message +// and a stack trace at the point Wrap was called. If the provided error is nil, +// Wrap returns nil. +// +// Usage example: +// +// err := errors.New("file not found") +// wrappedErr := Wrap(err, "Failed to read the file") +// fmt.Println(wrappedErr) // "Failed to read the file: file not found" with stack trace +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &underlyingMessage{ + cause: err, + msg: message, + } + return &underlyingStack{ + err, + Callers(), + } +} + +// Wrapf returns an error that annotates the provided error with a formatted message +// and a stack trace at the point Wrapf was called. If the provided error is nil, +// Wrapf returns nil. +// +// Usage example: +// +// err := errors.New("file not found") +// wrappedErr := Wrapf(err, "Failed to load file %s", filename) +// fmt.Println(wrappedErr) // "Failed to load file : file not found" with stack trace +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &underlyingMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &underlyingStack{ + err, + Callers(), + } +} + +// WithMessage annotates an existing error with a new message. If the error is nil, +// it returns nil. +// +// Usage example: +// +// err := errors.New("original error") +// errWithMessage := WithMessage(err, "Additional context") +// fmt.Println(errWithMessage) // "Additional context: original error" +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &underlyingMessage{ + cause: err, + msg: message, + } +} + +// WithMessagef annotates an existing error with a formatted message. If the error +// is nil, it returns nil. +// +// Usage example: +// +// err := errors.New("original error") +// errWithMessage := WithMessagef(err, "Context: %s", "something went wrong") +// fmt.Println(errWithMessage) // "Context: something went wrong: original error" +func WithMessagef(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + return &underlyingMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } +} + +// Cause traverses the error chain and returns the underlying cause of the error +// if it implements the `Cause()` method. If the error doesn't implement `Cause()`, +// it simply returns the original error. If the error is nil, nil is returned. +// +// Usage example: +// +// err := Wrap(errors.New("file not found"), "Failed to open file") +// causeErr := Cause(err) +// fmt.Println(causeErr) // "file not found" +// +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} + +// Error implements the error interface for the `underlying` type, returning the +// message stored in the error object. It is used to retrieve the error message. +// +// Usage example: +// +// err := WithError("Something went wrong") +// fmt.Println(err.Error()) // "Something went wrong" +func (u *underlying) Error() string { + return u.msg +} + +// Format formats the error according to the specified format verb. If the `+` +// flag is provided, it will format both the message and the stack trace. Otherwise, +// it will format just the message. +// +// Usage example: +// +// err := WithError("Something went wrong") +// fmt.Printf("%+v\n", err) // "Something went wrong" with stack trace +func (u *underlying) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, u.msg) + u.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, u.msg) + case 'q': + fmt.Fprintf(s, "%q", u.msg) + } +} + +// Cause returns the underlying error cause for the `underlyingStack` type. +func (u *underlyingStack) Cause() error { return u.error } + +// Unwrap returns the underlying error for the `underlyingStack` type. +func (u *underlyingStack) Unwrap() error { return u.error } + +// Format formats the error with the stack trace and the error message. If the +// `+` flag is set, it will include the stack trace as well. +// +// Usage example: +// +// err := Wrap(errors.New("file not found"), "Failed to open file") +// fmt.Printf("%+v\n", err) // "Failed to open file: file not found" with stack trace +func (u *underlyingStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", u.Cause()) + u.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, u.Error()) + case 'q': + fmt.Fprintf(s, "%q", u.Error()) + } +} + +// Cause returns the underlying cause of the error for the `underlyingMessage` type. +func (u *underlyingMessage) Cause() error { return u.cause } + +// Unwrap returns the underlying cause of the error for the `underlyingMessage` type. +func (u *underlyingMessage) Unwrap() error { return u.cause } + +// Error returns the error message concatenated with the underlying error message +// for the `underlyingMessage` type. +// +// Usage example: +// +// err := WithMessage(errors.New("file not found"), "Failed to open file") +// fmt.Println(err) // "Failed to open file: file not found" +func (u *underlyingMessage) Error() string { + return u.msg + ": " + u.cause.Error() +}