Skip to content

Commit

Permalink
🔇 silent changes: add base functions wrap errors #4
Browse files Browse the repository at this point in the history
  • Loading branch information
pnguyen215 committed Dec 9, 2024
1 parent 073e0be commit 18a40ea
Show file tree
Hide file tree
Showing 3 changed files with 343 additions and 9 deletions.
1 change: 1 addition & 0 deletions test/wraperr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package test
101 changes: 92 additions & 9 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
250 changes: 250 additions & 0 deletions wraperr.go
Original file line number Diff line number Diff line change
@@ -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 <filename>" 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 <filename>: 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()
}

0 comments on commit 18a40ea

Please sign in to comment.