Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 30 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import (
)

func main() {
err := errors.New("database error").
var ErrDB = errors.New("database error")
err := errors.From(ErrDB).
WithIdentifier(1001).
WithDetail("connection timeout").
WithProperty("host", "localhost").
Expand All @@ -40,45 +41,40 @@ func main() {

## Usage Examples

### Creating Basic Errors

```go
// Simple error
err := errors.New("validation failed").Throw()

// Error with identifier
err := errors.New("not found").
WithIdentifier(404).
Throw()
```

### Adding Details and Properties

```go
// Initializing Domain errors
var (
ErrValidationFailed = errors.New("validation failed")
ErrRequestFailed = errors.New("request failed")
ErrDatabaseError = errors.New("database error")
)

// Multiple details
// Each WithDetail() call appends to the Details slice
err := errors.New("validation failed").
err1 := errors.From(ErrValidationFailed).
WithDetail("email is required").
WithDetail("password must be at least 8 characters").
Throw()

// Details can also be formatted
err := errors.New("request failed").
err2 := errors.From(ErrRequestFailed).
WithDetailf("failed to connect to %s:%d", "api.example.com", 443).
WithDetail("timeout after 30 seconds").
Throw()

// Single property
err := errors.New("request failed").
err3 := errors.From(ErrRequestFailed).
WithProperty("url", "https://api.example.com").
WithProperty("status_code", 500).
Throw()

// Multiple properties at once
err := errors.New("database error").
err4 := errors.From(ErrDatabaseError).
WithProperties(map[string]any{
"host": "localhost",
"port": 5432,
"host": "localhost",
"port": 5432,
"database": "myapp",
}).
Throw()
Expand All @@ -87,11 +83,13 @@ err := errors.New("database error").
### Error Wrapping

```go
func getUserByID(id int) (*User, error) {
var ErrUserNotFound = errors.New("user not found")

func getUserByID(id string) (*User, error) {
user, err := db.Query(id)
if err != nil {
return nil, errors.New("user not found").
WithIdentifier(404).
return nil, errors.From(ErrUserNotFound).
WithIdentifier(404000).
CausedBy(err).
Throw()
}
Expand Down Expand Up @@ -120,16 +118,18 @@ func getUser(id int) (*User, error) {
### Stack Traces

```go
var ErrSomethingWentWrong = errors.New("something went wrong")

func layer3() error {
return errors.New("something went wrong").Throw()
return errors.From(ErrSomethingWentWrong).Throw()
}

func layer2() error {
err := layer3()
if err != nil {
return errors.Stamp(err) // Adds layer2's location to stack
}
return nil
err := layer3()
if err != nil {
return errors.Stamp(err) // Adds layer2's location to stack
}
return nil
}

func layer1() error {
Expand All @@ -147,7 +147,7 @@ func layer1() error {

```go
// Using errors.Is for comparison
notFoundErr := errors.New("not found").WithIdentifier(404)
notFoundErr := errors.New("not found")

if errors.Is(err, notFoundErr) {
// Handle not found error
Expand All @@ -165,9 +165,6 @@ if errors.As(err, &e) {
fmt.Printf(" Detail %d: %s\n", i, detail)
}
}

// Unwrapping errors
cause := errors.Unwrap(err)
```

### Converting Standard Errors
Expand All @@ -179,7 +176,7 @@ err := errors.From(stdErr).
WithDetail("additional context").
Throw()

// Using Intercept() when you're unsure of the error type
// Using Intercept() to complete the error
func handleError(err error) error {
e := errors.Intercept(err)
e.WithProperty("handled_at", time.Now())
Expand Down
13 changes: 11 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,14 @@ func New(title string) error {

// Wrap wraps an error with a message.
func Wrap(err error, msg string) error {
return From(err).WithDetail(msg).Throw()
trace := trace()
return from(err, true).WithDetail(msg).throw(trace)
}

// Wrapf wraps an error with a formatted message.
func Wrapf(err error, format string, args ...any) error {
return From(err).WithDetailf(format, args...).Throw()
trace := trace()
return from(err, true).WithDetailf(format, args...).throw(trace)
}

// From creates a new *Error from any error type.
Expand All @@ -76,6 +78,10 @@ func Wrapf(err error, format string, args ...any) error {
// If the error is an *Error, it returns a copy of the original error with the same
// title, identifier, details, properties.
func From(err error) *Error {
return from(err, false)
}

func from(err error, copyStack bool) *Error {
var t *Error

ok := errors.As(err, &t)
Expand All @@ -89,6 +95,9 @@ func From(err error) *Error {
Details: t.Details,
Properties: t.Properties,
}
if copyStack {
e.Stack = t.Stack
}

if !ok {
e.Cause = err
Expand Down
5 changes: 2 additions & 3 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ var _ = Describe("Errors", func() {
e := Intercept(err2)
// From() only preserves title, so only the latest detail is kept
Expect(e.Details).To(Equal([]string{"database query failed", "user service error"}))
Expect(e.Stack).To(HaveLen(1))
Expect(e.Stack).To(HaveLen(2))
// From() on *Error doesn't set cause
Expect(e.Cause).To(BeNil())
})
Expand Down Expand Up @@ -424,8 +424,7 @@ var _ = Describe("Errors", func() {
err2 := Wrap(err1, "duplicate entry detected")
e := Intercept(err2)
Expect(e.Details).To(Equal([]string{"database error: code 1062", "duplicate entry detected"}))
Expect(e.Stack).To(HaveLen(1))
// From() on *Error doesn't set cause
Expect(e.Stack).To(HaveLen(2))
Expect(e.Cause).To(BeNil())
})

Expand Down