Skip to content

Commit

Permalink
Add Trace options
Browse files Browse the repository at this point in the history
  • Loading branch information
alnvdl committed Mar 14, 2023
1 parent 9f1e493 commit 953a5bf
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 64 deletions.
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@ Without terr | With terr
`errors.New("error")` | `terr.Newf("error")`
`fmt.Errorf("error: %w", err)` | `terr.Newf("error: %w", err)`
`[return] err` | `terr.Trace(err)`
`[return] &CustomError{}` | `terr.TraceWithLocation(&CustomError{}, ...)`
`[return] &CustomError{}` | `terr.Trace(&CustomError{}, ...opts)`

`terr.Newf` can receive multiple errors. In fact, it is just a very slim
wrapper around `fmt.Errorf`. Any traced error passed to `terr.Newf` will be
included in the error tracing tree, regardless of the `fmt` verb used for it.

`terr.Trace` and `terr.TraceWithLocation` on the other hand do nothing with the
error they receive (no wrapping and no masking), but they add one level to the
error tracing tree.
`terr.Trace` on the other hand does nothing with the error it receive (no
wrapping and no masking), but it adds one level to the error tracing tree.
`terr.Trace` can also receive additional options to support custom errors:
`terr.WithLocation` and `terr.WithChildren`, which allow changing attributes of
the traced error.

To obtain the full trace, terr functions must be used consistently. If
`fmt.Errorf` is used at one point, the error tracing information will be reset
Expand All @@ -54,7 +56,8 @@ replacing the `terr` function calls with equivalent expressions.

### Tracing custom errors
Custom errors can be turned into traced errors as well by using
`terr.TraceWithLocation` in constructor functions. An example is available[^3].
`terr.Trace(err, terr.WithLocation(file, line))` in constructor functions. An
example is available[^3].

### Printing errors
An error tracing tree can be printed with the special `%@` formatting verb. An
Expand Down Expand Up @@ -104,13 +107,13 @@ $ gofmt -w -r 'fmt.Errorf -> terr.Newf' .
$ goimports -w .
```

Adopting `terr.Trace` and `terr.TraceWithLocation` is harder, as it is
impossible to write a simple gofmt rewrite rule that works for all cases. So
these two functions have to be applied by hand following these guidelines:
Adopting `terr.Trace` is harder, as it is impossible to write a simple gofmt
rewrite rule that works for all cases. So it has to be applied by hand
following these guidelines:
- `return err` becomes `return terr.Trace(err)`;
- `return NewCustomErr()` requires an adaptation in `NewCustomErr` to use
`terr.TraceWithLocation`. An example is available[^3].
`return terr.TraceWithLocation(NewCustomErr())`.
`terr.Trace`, possibly with options (usually only `terr.WithLocation`, but
`terr.WithChildren` is also available). An example is available[^3].

### Getting rid of terr
Run the following commands in a directory tree to get rid of terr in all its
Expand All @@ -119,14 +122,15 @@ files (make sure `goimports`[^6] is installed first):
$ gofmt -w -r 'terr.Newf(a) -> errors.New(a)' .
$ gofmt -w -r 'terr.Newf -> fmt.Errorf' .
$ gofmt -w -r 'terr.Trace(a) -> a' .
$ gofmt -w -r 'terr.TraceWithLocation(a, b, c) -> a' .
$ gofmt -w -r 'terr.Trace(a, b) -> a' .
$ gofmt -w -r 'terr.Trace(a, b, c) -> a' .
$ goimports -w .
$ go mod tidy
```

[^1]: https://go.dev/blog/go1.13-errors
[^2]: https://pkg.go.dev/github.com/alnvdl/terr#pkg-examples
[^3]: https://pkg.go.dev/github.com/alnvdl/terr#example-TraceWithLocation
[^3]: https://pkg.go.dev/github.com/alnvdl/terr#example-Trace-CustomError
[^4]: https://pkg.go.dev/github.com/alnvdl/terr#example-package
[^5]: https://pkg.go.dev/github.com/alnvdl/terr#example-TraceTree
[^6]: https://pkg.go.dev/golang.org/x/tools/cmd/goimports
10 changes: 5 additions & 5 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ func (e *ValidationError) Error() string {

func NewValidationError(msg string) error {
_, file, line, _ := runtime.Caller(1)
return terr.TraceWithLocation(&ValidationError{msg}, file, line)
return terr.Trace(&ValidationError{msg}, terr.WithLocation(file, line))
}

// This example shows how to adding tracing information to custom error types
// using TraceWithLocation. Custom error type constructors like
// This example shows how to add tracing information to custom error types
// using Trace and the WithLocation option. Custom error type constructors like
// NewValidationError can define a location for the errors they return. In this
// case, the location is being set it to the location of the NewValidationError
// case, the location is being set to the location of the NewValidationError
// caller.
func ExampleTraceWithLocation() {
func ExampleTrace_customError() {
// err will be annotated with the line number of the following line.
err := NewValidationError("x must be >= 0")
fmt.Printf("%@\n", err)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/alnvdl/terr

// Incomplete and/or incorrect release (should have been v0).
retract [v1.0.0, v1.0.11]
retract [v1.0.0, v1.0.12]

go 1.20
75 changes: 41 additions & 34 deletions terr.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,14 @@ func getCallerLocation() location {
return location{file, line}
}

func newTracedError(err error, children []error, loc location) error {
var terrs []ErrorTracer
func newTracedError(err error, children []any, loc location) *tracedError {
terr := &tracedError{err, loc, nil}
for _, child := range children {
if terr, ok := child.(*tracedError); ok {
terrs = append(terrs, terr)
if child, ok := child.(*tracedError); ok {
terr.children = append(terr.children, child)
}
}
return &tracedError{err, loc, terrs}
}

func filterErrors(objs []interface{}) []error {
var errors []error
for _, o := range objs {
if err, ok := o.(error); ok {
errors = append(errors, err)
}
}
return errors
return terr
}

// Is returns whether the error is another error for use with errors.Is.
Expand All @@ -53,7 +43,7 @@ func (e *tracedError) Is(target error) bool {
}

// As returns the error as another error for use with errors.As.
func (e *tracedError) As(target interface{}) bool {
func (e *tracedError) As(target any) bool {
return errors.As(e.error, target)
}

Expand Down Expand Up @@ -111,32 +101,49 @@ func treeRepr(err error, depth int) []string {
// of the formatting verbs used for these errors.
// This function is equivalent to fmt.Errorf("...", ...). If used without verbs
// and additional arguments, it is equivalent to errors.New("...").
func Newf(format string, a ...interface{}) error {
return newTracedError(
fmt.Errorf(format, a...),
filterErrors(a),
getCallerLocation(),
)
func Newf(format string, a ...any) error {
return newTracedError(fmt.Errorf(format, a...), a, getCallerLocation())
}

// Trace returns a new traced error for err. If err is already a traced error,
// a new traced error will be returned containing err as a child traced error.
// No wrapping or masking takes place in this function.
func Trace(err error) error {
if err == nil {
return nil
// A TraceOption allows customization of errors returned by the Trace function.
type TraceOption func(e *tracedError)

// WithLocation returns a traced error with the given location. This option can
// be used in custom error constructor functions, so they can return a traced
// error pointing at their callers.
func WithLocation(file string, line int) TraceOption {
return func(e *tracedError) {
e.location = location{file, line}
}
}

// WithChildren returns a traced error with the given traced errors appended as
// children Non-traced errors are ignored. This option can be used in custom
// error constructor functions to define the children traced errors for a
// traced error.
func WithChildren(children []error) TraceOption {
return func(e *tracedError) {
for _, child := range children {
if terr, ok := child.(*tracedError); ok {
e.children = append(e.children, terr)
}
}
}
return newTracedError(err, []error{err}, getCallerLocation())
}

// TraceWithLocation works like Trace, but lets the caller specify a file and
// line for the error. This function can be used in custom error constructor
// functions, so they can return a traced error pointing at their callers.
func TraceWithLocation(err error, file string, line int) error {
// Trace returns a new traced error for err. If err is already a traced error,
// a new traced error will be returned containing err as a child traced error.
// opts is an optional series of TraceOptions to be applied to the traced
// error. No wrapping or masking takes place in this function.
func Trace(err error, opts ...TraceOption) error {
if err == nil {
return nil
}
return newTracedError(err, []error{err}, location{file, line})
terr := newTracedError(err, []any{err}, getCallerLocation())
for _, opt := range opts {
opt(terr)
}
return terr
}

// ErrorTracer is an object capable of tracing an error's location and possibly
Expand Down
28 changes: 16 additions & 12 deletions terr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,22 @@ func TestTrace(t *testing.T) {
fmt.Sprintf("fail @ %s:%d", file, line+2),
fmt.Sprintf("\tfail @ %s:%d", file, line+1),
}, "\n"))
}

func TestTraceWithLocation(t *testing.T) {
err := terr.TraceWithLocation(errors.New("custom"), "somefile.go", 123)

assertEquals(t, err.Error(), "custom")
// err.Unwrap() should still return nil, because no wrapping took place.
assertErrorIsNil(t, errors.Unwrap(err))
assertEquals(t, fmt.Sprintf("%@", err), "custom @ somefile.go:123")

wrappedErr := terr.Newf("%w", err)
assertEquals(t, errors.Is(wrappedErr, err), true)
tracedErrOpts := terr.Trace(err,
terr.WithLocation("somefile.go", 123),
terr.WithChildren([]error{tracedErr}),
)
assertEquals(t, tracedErrOpts.Error(), "fail")
assertEquals(t, errors.Is(tracedErrOpts, err), true)
// tracedErr.Unwrap() should still return nil, because no wrapping took place.
assertErrorIsNil(t, errors.Unwrap(tracedErrOpts))
assertEquals(t, fmt.Sprintf("%@", tracedErrOpts), strings.Join([]string{
fmt.Sprintf("fail @ %s:%d", "somefile.go", 123),
fmt.Sprintf("\tfail @ %s:%d", file, line+1),
// tracedErrOpts included tracedErr as a child.
fmt.Sprintf("\tfail @ %s:%d", file, line+2),
fmt.Sprintf("\t\tfail @ %s:%d", file, line+1),
}, "\n"))
}

type customError struct {
Expand Down Expand Up @@ -197,7 +201,7 @@ func TestNewfMultiple(t *testing.T) {

func TestNil(t *testing.T) {
assertErrorIsNil(t, terr.Trace(nil))
assertErrorIsNil(t, terr.TraceWithLocation(nil, "somefile.go", 123))
assertErrorIsNil(t, terr.Trace(nil, terr.WithLocation("somefile.go", 123)))

assertTraceTreeEquals(t, terr.TraceTree(nil), nil)
}

0 comments on commit 953a5bf

Please sign in to comment.