Skip to content

Commit

Permalink
Improve examples
Browse files Browse the repository at this point in the history
  • Loading branch information
alnvdl committed Mar 12, 2023
1 parent 3a1f56f commit 3c8710a
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 61 deletions.
60 changes: 32 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/alnvdl/terr.svg)](https://pkg.go.dev/github.com/alnvdl/terr)
[![Test workflow](https://github.com/alnvdl/terr/actions/workflows/test.yaml/badge.svg)](https://github.com/alnvdl/terr/actions/workflows/test.yaml)

terr (short for **t**raced **err**or) is a minimalistic library for adding
terr (short for **t**raced **err**or) is a minimalistic package for adding
error tracing in Go 1.20+.

Go's error representation primitives introduced in Go 1.13[^1] are quite
Expand All @@ -15,11 +15,11 @@ but it adds two features:
- file and line information for tracing errors;
- the ability to print error trees using the `fmt` package with the `%@` verb;

This library introduces the concept of **traced errors**: a wrapper for errors
This package introduces the concept of **traced errors**: a wrapper for errors
that includes the location where they were created (`errors.New`), passed along
(`return err`), wrapped (`%w`) or masked (`%v`). Traced errors keep track of
children traced errors that relate to them. An error is a traced error if it
was returned by one of the functions of this library.
was returned by one of the functions of this package.

Traced errors work seamlessly with `errors.Is`, `errors.As` and `errors.Unwrap`
just as if terr were not being used.
Expand All @@ -30,7 +30,7 @@ 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] NewCustomErr()` | `terr.TraceWithLocation(NewCustomErr())`
`[return] NewCustomErr()` | `terr.TraceWithLocation(NewCustomErr(), ...)`

`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
Expand All @@ -40,11 +40,20 @@ included in the traced error tree, regardless of the `fmt` verb used for it.
the error they receives (no wrapping and no masking), but they add one level
to the error tracing tree.

A couple of examples are available showing these functions in use[^2] [^3].
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
at that point, but Go's wrapped error tree will be preserved even in that case.

Examples are available showing all these functions in use[^2].

In the glorious day error tracing is added to Go, and assuming it gets done in
a way that respects error handling as defined in Go 1.13+,
[removing `terr` from a codebase](#getting-rid-of-terr) should be a matter of
replacing the `terr` function calls with the equivalent documented expressions.

### Printing errors
A traced error tree can be printed with the special `%@` formatting verb. An
example is available[^2].
example is available[^3].

`%@` prints the error tree in a tab-indented, multi-line representation. If a
custom format is needed (e.g., JSON), it is possible to implement a function
Expand All @@ -53,9 +62,14 @@ the [next subsection](#walking-the-traced-error-tree).

### 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.TraceWithLocation` in constructor functions. An example is available[^4].

### Walking the traced error tree
Starting with Go 1.20, wrapped errors are kept as a n-ary tree. terr works by
building a tree containing tracing information in parallel, leaving the Go
error tree untouched, as if terr weren't being used. Each traced error is thus
a node of this parallel traced error tree.

`terr.TraceTree(err) TracedError` can be used to obtain the root of an n-ary
traced error tree, which can be navigated using the following methods:
```go
Expand All @@ -77,9 +91,11 @@ Methods provided by the by the Go standard library should be used to walk Go's
wrapped error tree, which would includes non-traced errors and ignores masked
errors (e.g., `errors.Unwrap`).

An example is available[^5].

### Adopting terr
Run the following commands in a folder to recursively adopt terr in all its
files (make sure `goimports`[^4] is installed first):
files (make sure `goimports`[^6] is installed first):
```sh
$ go get github.com/alnvdl/terr
$ gofmt -w -r 'errors.New(a) -> terr.Newf(a)' .
Expand All @@ -92,12 +108,13 @@ impossible to write a simple gofmt rewrite rule that works for all cases.
Therefore, `terr.Trace` and `terr.TraceWithLocation` have to be applied as
needed in a code base. A rough guideline would be:
- `return err` becomes `return terr.Trace(err)`;
- `return NewCustomErr()` becomes
- `return NewCustomErr()` requires an adaptation in `NewCustomErr` to use
`terr.TraceWithLocation`. An example is available[^4].
`return terr.TraceWithLocation(NewCustomErr())`.

### Getting rid of terr
Run the following commands in a folder to recursively get rid of terr in all
its files (make sure `goimports`[^4] is installed first):
its files (make sure `goimports`[^6] is installed first):
```sh
$ gofmt -w -r 'terr.Newf(a) -> errors.New(a)' .
$ gofmt -w -r 'terr.Newf -> fmt.Errorf' .
Expand All @@ -107,22 +124,9 @@ $ goimports -w .
$ go mod tidy
```

## Under the hood
Starting with Go 1.20, wrapped errors are kept as a n-ary tree. terr works by
building a tree containing tracing information in parallel, leaving the Go
error tree untouched, as if terr weren't being used. Each traced error is thus
a node of this parallel traced error tree.

In the glorious day error tracing is added to Go, and assuming it gets done in
a way that respects error handling as defined in Go 1.13+,
[removing `terr` from a codebase](#getting-rid-of-terr) should be a matter of
replacing the `terr` function calls with the equivalent documented expressions.

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
at that point, but Go's wrapped error tree will be preserved even in that case.

[^1]: https://go.dev/blog/go1.13-errors
[^2]: https://pkg.go.dev/github.com/alnvdl/terr#example-package
[^3]: https://pkg.go.dev/github.com/alnvdl/terr#example-TraceWithLocation
[^4]: https://pkg.go.dev/golang.org/x/tools/cmd/goimports
[^2]: https://pkg.go.dev/github.com/alnvdl/terr#pkg-examples
[^3]: https://pkg.go.dev/github.com/alnvdl/terr#example-package
[^4]: https://pkg.go.dev/github.com/alnvdl/terr#example-TraceWithLocation
[^5]: https://pkg.go.dev/github.com/alnvdl/terr#example-TraceTree
[^6]: https://pkg.go.dev/golang.org/x/tools/cmd/goimports
92 changes: 90 additions & 2 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,105 @@
package terr_test

import (
"errors"
"fmt"
"runtime"

"github.com/alnvdl/terr"
)

// This example shows how to call different terr functions and print a traced
// error tree at the end.
// This example shows how to combine different terr functions and print a
// traced error tree at the end.
func Example() {
err := terr.Newf("base")
traced := terr.Trace(err)
wrapped := terr.Newf("wrapped: %w", traced)
masked := terr.Newf("masked: %v", wrapped)
fmt.Printf("%@\n", masked)
}

// This example shows how Newf interacts with a non-traced error compared to
// when it receives a traced error. Traced errors are included in the trace
// regardless of the fmt verb used for them, while non-traced errors are
// handled as usual, but do not get included in the trace.
func ExampleNewf() {
nonTracedErr := errors.New("non-traced")
tracedErr1 := terr.Newf("traced 1")
tracedErr2 := terr.Newf("traced 2")
newErr := terr.Newf("errors: %w, %v, %w",
nonTracedErr,
tracedErr1,
tracedErr2)

fmt.Printf("%@\n", newErr)
fmt.Println("---")

// errors.Is works.
fmt.Println("newErr is nonTracedErr:", errors.Is(newErr, nonTracedErr))
fmt.Println("newErr is tracedErr1:", errors.Is(newErr, tracedErr1))
fmt.Println("newErr is tracedErr2:", errors.Is(newErr, tracedErr2))
}

// This example shows how terr.Trace interacts with a non-traced error compared
// to when it receives a traced error.
func ExampleTrace() {
nonTracedErr := errors.New("non-traced")
tracedErr := terr.Newf("traced")

fmt.Printf("%@\n", terr.Trace(nonTracedErr))
fmt.Println("---")
fmt.Printf("%@\n", terr.Trace(tracedErr))
}

type ValidationError struct{ msg string }

func (e *ValidationError) Error() string {
return e.msg
}

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

// This example shows how to adding tracing information to custom error types
// using TraceWithLocation. Custom errors constructors like NewValidationError
// can define a location for the errors they return. In this case, that
// location is being set it to the location of the NewValidationError caller.
func ExampleTraceWithLocation() {
// err will be annotated with the line number of the following line.
err := NewValidationError("x must be >= 0")
fmt.Printf("%@\n", err)
fmt.Println("---")

// error.As works.
var customErr *ValidationError
ok := errors.As(err, &customErr)
fmt.Println("Is ValidationError:", ok)
fmt.Println("Custom error message:", customErr.msg)
}

// This example shows how to use the n-ary traced error tree returned by
// terr.TraceTree.
func ExampleTraceTree() {
nonTracedErr := errors.New("non-traced")
tracedErr1 := terr.Newf("traced 1")
tracedErr2 := terr.Newf("traced 2")
newErr := terr.Newf("%w, %v, %w",
nonTracedErr,
tracedErr1,
tracedErr2)

printNode := func(node terr.TracedError) {
fmt.Printf("Error: %v\n", node.Error())
file, line := node.Location()
fmt.Printf("Location: %s:%d\n", file, line)
fmt.Printf("Children: %v\n", node.Children())
fmt.Println("---")
}

node := terr.TraceTree(newErr)
printNode(node)
printNode(node.Children()[0])
printNode(node.Children()[1])
}
29 changes: 0 additions & 29 deletions example_tracewithlocation_test.go

This file was deleted.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module github.com/alnvdl/terr

retract [v0.0.0, v1.0.7] // Incomplete and/or wrong releases.
retract [v0.0.0, v1.0.8] // Incomplete and/or wrong releases.

go 1.20
2 changes: 1 addition & 1 deletion terr.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type TracedError interface {
}

// tracedError implements the TracedError interface while being compatible with
// functions from the standard library "errors" package.
// functions from the "errors" package in the standard library.
type tracedError struct {
error
location
Expand Down

0 comments on commit 3c8710a

Please sign in to comment.