Skip to content
Open
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
177 changes: 176 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,182 @@ go get github.com/maratori/errors

## Usage

TBD
### Example 1

- `errors.New` creates new error with message
- `errors.Wrap` adds string prefix to an error
- `errors.WithField` adds key-value pair to an error
- `errors.FieldsFromError` extracts key-value pairs from an error

```go
package pkg

import (
"os"

"github.com/maratori/errors"
"golang.org/x/exp/slog"
)

func Example1() {
logger := slog.New(slog.NewJSONHandler(os.Stdout))

err := svc.ProcessRequest(request)
if err != nil {
var fields []any
for key, value := range errors.FieldsFromError(err) {
fields = append(fields, slog.Any(key, value))
}
logger.Error("can't process request", err, fields...)
// {
// "msg": "can't process request",
// "err": "can't borrow money: limit reached",
// "balance": 100,
// "requested": 50,
// "limit": 120,
// "request": ...
// }
}
}

func (s *Service) ProcessRequest(request Request) error {
switch r := request.(type) {
case *BorrowMoneyRequest:
err := s.BorrowMoney(r)
return errors.Wrap("can't borrow money", err).
WithField("request", r).E()
}
return nil
}

func (s *Service) BorrowMoney(request *BorrowMoneyRequest) error {
wallet, err := s.repository.GetWallet(request.WalletID)
if err != nil {
return errors.Wrap("can't get wallet", err).
WithField("wallet_id", request.WalletID).E()
}

if wallet.Balance+request.Amount > wallet.Limit {
return errors.New("limit reached").WithFields(errors.Fields{
"balance": wallet.Balance,
"requested": request.Amount,
"limit": wallet.Limit,
}).E()
}

return nil
}
```

### Example 2

- `errors.AppendInto` and `errors.Join` joins several errors into single one
- `errors.Errros` extracts errors from an error, see [error tree](#error-tree)

```go
package pkg

import (
"os"

"github.com/maratori/errors"
"golang.org/x/exp/slog"
)

func Example2() {
logger := slog.New(slog.NewJSONHandler(os.Stdout))

err := svc.ProcessRequest(request)
for _, e := range errors.Errors(err) { // extract all errors
var fields []any
for key, value := range errors.FieldsFromError(e) {
fields = append(fields, slog.Any(key, value))
}
logger.Error("can't process request", e, fields...)
}
}

func (s *Service) ProcessRequest(request Request) error {
switch r := request.(type) {
case *PingFriendsRequest:
err := s.PingFriends(r)
return errors.Wrap("can't ping friends", err).
WithField("request", r).E()
}
return nil
}

func (s *Service) PingFriends(request *PingFriendsRequest) error {
user, err := s.repository.GetUser(request.UserID)
if err != nil {
return errors.Wrap("can't get user", err).
WithField("user_id", request.UserID).E()
}

var errs error
for _, friend := range user.Friends {
err := s.pingFriend(user, friend)
errors.AppendInto(&errs, errors.Wrap("can't ping friend", err).
WithField("user", user).
WithField("friend", friend).E())
}
return errs

// Alternative:

// var errs []error
// for _, friend := range user.Friends {
// err := s.pingFriend(user, friend)
// errs = append(errs, errors.Wrap("can't ping friend", err).
// WithField("user", user).
// WithField("friend", friend).E())
// }
// return errors.Join(errs...)
}
```

## Error tree

Functions from this library create errors tree under the hood. Let's say you have the following chain of calls:

```mermaid
graph TD
A("a := errors.Wrap(#quot;a#quot;, b).\nWithField(#quot;A#quot;, 50).E()") --> B("b := errors.Join(c, d)")
B --> C("c := errors.New(#quot;c#quot;).\nWithField(#quot;C#quot;, 40).E()")
B --> D("d := errors.Wrap(#quot;d#quot;, e).\nWithField(#quot;D#quot;, 30).\nWithField(#quot;Repeated#quot;, 7).E()")
D --> E("e := errors.Join(f, g)")
E --> F("f := errors.Wrap(#quot;f#quot;, h).\nWithField(#quot;F#quot;, 20).E()")
E --> G("g := errors.New(#quot;g#quot;).\nWithField(#quot;G#quot;, 10).\nWithField(#quot;Repeated#quot;, 4).E()")
F --> H("h := fmt.Errorf(#quot;x#quot;)")
```

`errors.Errors` will return 3 errors (all possible paths from the root to leafs):

```go
errs := errors.Errors(a)
assert.Len(t, errs, 3)
assert.EqualError(t, errs[0], "a: c")
assert.EqualError(t, errs[1], "a: d: f: x")
assert.EqualError(t, errs[2], "a: d: g")
```

Each error can be passed to `errors.FieldsFromError` to get joined fields from all corresponding nodes. For repeated fields the most inner one has priority.

```go
assert.Equal(t,
errors.Fields{"A": 50, "C": 40},
errors.FieldsFromError(errs[0]))
assert.Equal(t,
errors.Fields{"A": 50, "D": 30, "Repeated": 7, "F": 20},
errors.FieldsFromError(errs[1]))
assert.Equal(t,
errors.Fields{"A": 50, "D": 30, "Repeated": 4, "G": 10},
errors.FieldsFromError(errs[2]))
```

> **Note 1.** If the tree is a linear graph (i.e. `errors.Join` or `errors.AppendInto` was not used), `errors.Errors` returns the single error (the original one).

> **Note 2.** If the tree is not a linear graph, `errors.FieldsFromError` returns fields from the path to the first leaf. I.e. `errors.FieldsFromError(err) == errors.FieldsFromError(errors.Errors(err)[0])`

## Contribution

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
module github.com/maratori/errors

go 1.19
go 1.20

require github.com/stretchr/testify v1.8.1

Expand Down