Skip to content

Commit

Permalink
feat: allow error output and stack trace inversion (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
morningvera authored Feb 13, 2020
1 parent 17b0393 commit bd2da70
Show file tree
Hide file tree
Showing 7 changed files with 413 additions and 216 deletions.
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ lint:
@echo Linting
@golangci-lint run --no-config --issues-exit-code=0 --timeout=5m

## Format docs
docs:
@echo Formatting docs
@npm list -g markdown-toc > /dev/null 2>&1 || npm install -g markdown-toc > /dev/null 2>&1
@markdown-toc -i README.md

## Run the tests
test:
@echo Running tests
Expand Down
70 changes: 49 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Package `eris` provides a better way to handle, trace, and log errors in Go.
* [Wrapping errors](#wrapping-errors)
* [Formatting and logging errors](#formatting-and-logging-errors)
* [Interpreting eris stack traces](#interpreting-eris-stack-traces)
* [Inverting the stack trace and error output](#inverting-the-stack-trace-and-error-output)
* [Inspecting errors](#inspecting-errors)
* [Formatting with custom separators](#formatting-with-custom-separators)
* [Writing a custom output format](#writing-a-custom-output-format)
Expand All @@ -29,28 +30,28 @@ Package `eris` provides a better way to handle, trace, and log errors in Go.

Named after the Greek goddess of strife and discord, this package is designed to give you more control over error handling via error wrapping, stack tracing, and output formatting. `eris` was inspired by a simple question: what if you could fix a bug without wasting time replicating the issue or digging through the code?

`eris` is intended to help developers diagnose issues faster. The [example](https://github.com/rotisserie/eris/blob/master/examples/logging/example.go) that generated the output below simulates a realistic error handling scenario and demonstrates how to wrap and log errors with minimal effort. This specific error occurred because a user tried to access a file that can't be located, and the output shows a clear path from the source to the top of the call stack.
`eris` is intended to help developers diagnose issues faster. The [example](https://github.com/rotisserie/eris/blob/master/examples/logging/example.go) that generated the output below simulates a realistic error handling scenario and demonstrates how to wrap and log errors with minimal effort. This specific error occurred because a user tried to access a file that can't be located, and the output shows a clear path from the top of the call stack to the source.

```json
{
"error":{
"root":{
"message":"error internal server",
"stack":[
"main.GetRelPath:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:61",
"main.ProcessResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:82",
"main.main:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:143",
"main.ProcessResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:85",
"main.main:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:143"
"main.ProcessResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:82",
"main.GetRelPath:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:61"
]
},
"wrap":[
{
"message":"Rel: can't make ./some/malformed/absolute/path/data.json relative to /Users/roti/",
"stack":"main.GetRelPath:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:61"
},
{
"message":"failed to get relative path for resource 'res2'",
"stack":"main.ProcessResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:85"
},
{
"message":"Rel: can't make ./some/malformed/absolute/path/data.json relative to /Users/roti/",
"stack":"main.GetRelPath:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:61"
}
]
},
Expand Down Expand Up @@ -115,17 +116,17 @@ fmt.Println(formattedStr)

### Interpreting eris stack traces

Errors created with this package contain stack traces that are managed automatically. They're currently mandatory when creating and wrapping errors but optional when printing or logging. The stack trace and all wrapped layers follow the same order as Go's `runtime` package, which means that the root cause of the error is shown first.
Errors created with this package contain stack traces that are managed automatically. They're currently mandatory when creating and wrapping errors but optional when printing or logging. By default, the stack trace and all wrapped layers follow the opposite order of Go's `runtime` package, which means that the original calling method is shown first and the root cause of the error is shown last.

```golang
{
"root":{
"message":"error bad request", // root cause
"stack":[
"main.(*Request).Validate:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:28", // location of the root
"main.(*Request).Validate:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:29", // location of Wrap call
"main.main:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:143", // original calling method
"main.ProcessResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:71",
"main.main:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:143"
"main.(*Request).Validate:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:29", // location of Wrap call
"main.(*Request).Validate:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:28" // location of the root
]
},
"wrap":[
Expand All @@ -137,6 +138,31 @@ Errors created with this package contain stack traces that are managed automatic
}
```

### Inverting the stack trace and error output

If you prefer some other order than the default, `eris` supports inverting both the stack trace and the entire error output. When both are inverted, the root error is shown first and the original calling method is shown last.

```golang
// create a default format with error and stack inversion options
format := eris.NewDefaultStringFormat(eris.FormatOptions{
InvertOutput: true, // flag that inverts the error output (wrap errors shown first)
WithTrace: true, // flag that enables stack trace output
InvertTrace: true, // flag that inverts the stack trace output (top of call stack shown first)
})

// format the error to a string and print it
formattedStr := eris.ToCustomString(err, format)
fmt.Println(formattedStr)

// example output:
// error not found
// main.GetResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:52
// main.ProcessResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:76
// main.main:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:143
// failed to get resource 'res1'
// main.GetResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:52
```

### Inspecting errors

The `eris` package provides a couple ways to inspect and compare error types. [`eris.Is`](https://godoc.org/github.com/rotisserie/eris#Is) returns true if a particular error appears anywhere in the error chain. Currently, it works simply by comparing error messages with each other. If an error contains a particular message (e.g. `"error not found"`) anywhere in its chain, it's defined to be that error type.
Expand Down Expand Up @@ -165,12 +191,14 @@ if eris.Cause(err) == ErrNotFound {

### Formatting with custom separators

For users who need more control over the error output, `eris` allows for some control over the separators between each piece of the output via the [`eris.Format`](https://godoc.org/github.com/rotisserie/eris#Format) type. Currently, the default order of the error and stack trace output is rigid. If this isn't flexible enough for your needs, see the [custom output format](#writing-a-custom-output-format) section below. To format errors with custom separators, you can define and pass a format object to [`eris.ToCustomString`](https://godoc.org/github.com/rotisserie/eris#ToCustomString) or [`eris.ToCustomJSON`](https://godoc.org/github.com/rotisserie/eris#ToCustomJSON).
For users who need more control over the error output, `eris` allows for some control over the separators between each piece of the output via the [`eris.Format`](https://godoc.org/github.com/rotisserie/eris#Format) type. If this isn't flexible enough for your needs, see the [custom output format](#writing-a-custom-output-format) section below. To format errors with custom separators, you can define and pass a format object to [`eris.ToCustomString`](https://godoc.org/github.com/rotisserie/eris#ToCustomString) or [`eris.ToCustomJSON`](https://godoc.org/github.com/rotisserie/eris#ToCustomJSON).

```golang
// format the error to a string with custom separators
formattedStr := eris.ToCustomString(err, Format{
WithTrace: true, // flag that enables stack trace output
FormatOptions: eris.FormatOptions{
WithTrace: true, // flag that enables stack trace output
},
MsgStackSep: "\n", // separator between error messages and stack frame data
PreStackSep: "\t", // separator at the beginning of each stack frame
StackElemSep: " | ", // separator between elements of each stack frame
Expand All @@ -179,11 +207,11 @@ formattedStr := eris.ToCustomString(err, Format{
fmt.Println(formattedStr)

// example output:
// unexpected EOF
// error reading file 'example.json'
// main.readFile | .../example/main.go | 6
// main.parseFile | .../example/main.go | 12
// unexpected EOF
// main.main | .../example/main.go | 20
// error reading file 'example.json'
// main.parseFile | .../example/main.go | 12
// main.readFile | .../example/main.go | 6
```

Expand Down Expand Up @@ -227,15 +255,15 @@ sentry.CaptureMessage(uErr.ErrRoot.Msg)
Readability is a major design requirement for `eris`. In addition to the JSON output shown above, `eris` also supports formatting errors to a simple string.

```
error not found
failed to get resource 'res1'
main.GetResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:52
main.ProcessResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:76
error not found
main.main:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:143
failed to get resource 'res1'
main.ProcessResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:76
main.GetResource:/Users/roti/go/src/github.com/rotisserie/eris/examples/logging/example.go:52
```

The `eris` error stack is designed to be easier to interpret than other error handling packages, and it achieves this by omitting extraneous information and avoiding unnecessary repetition. The stack trace above omits calls from Go's `runtime` package and includes just a single frame for wrapped layers which are inserted into the root error stack trace in the correct order. `eris` also correctly handles and updates stack traces for global error values.
The `eris` error stack is designed to be easier to interpret than other error handling packages, and it achieves this by omitting extraneous information and avoiding unnecessary repetition. The stack trace above omits calls from Go's `runtime` package and includes just a single frame for wrapped layers which are inserted into the root error stack trace in the correct order. `eris` also correctly handles and updates stack traces for global error values in a transparent way.

The output of `pkg/errors` for the same error is shown below. In this case, the root error stack trace is incorrect because it was declared as a global value, and it includes several extraneous lines from the `runtime` package. The output is also much more difficult to read and does not allow for custom formatting.

Expand Down
22 changes: 11 additions & 11 deletions eris_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,27 @@ func TestErrorWrapping(t *testing.T) {
"standard error wrapping with a global root cause": {
cause: globalErr,
input: []string{"additional context", "even more context"},
output: "global error: additional context: even more context",
output: "even more context: additional context: global error",
},
"formatted error wrapping with a global root cause": {
cause: formattedGlobalErr,
input: []string{"additional context", "even more context"},
output: "formatted global error: additional context: even more context",
output: "even more context: additional context: formatted global error",
},
"standard error wrapping with a local root cause": {
cause: eris.New("root error"),
input: []string{"additional context", "even more context"},
output: "root error: additional context: even more context",
output: "even more context: additional context: root error",
},
"standard error wrapping with a local root cause (eris.Errorf)": {
cause: eris.Errorf("%v root error", "formatted"),
input: []string{"additional context", "even more context"},
output: "formatted root error: additional context: even more context",
output: "even more context: additional context: formatted root error",
},
"standard error wrapping with a third-party root cause (errors.New)": {
cause: errors.New("external error"),
input: []string{"additional context", "even more context"},
output: "external error: additional context: even more context",
output: "even more context: additional context: external error",
},
"no error wrapping with a local root cause (eris.Errorf)": {
cause: eris.Errorf("%v root error", "formatted"),
Expand Down Expand Up @@ -95,17 +95,17 @@ func TestErrorUnwrap(t *testing.T) {
cause: eris.New("root error"),
input: []string{"additional context", "even more context"},
output: []string{
"root error: additional context: even more context",
"root error: additional context",
"even more context: additional context: root error",
"additional context: root error",
"root error",
},
},
"unwrapping error with external root cause (errors.New)": {
cause: errors.New("external error"),
input: []string{"additional context", "even more context"},
output: []string{
"external error: additional context: even more context",
"external error: additional context",
"even more context: additional context: external error",
"additional context: external error",
"external error",
},
},
Expand Down Expand Up @@ -242,12 +242,12 @@ func TestErrorFormatting(t *testing.T) {
"standard error wrapping with internal root cause (eris.New)": {
cause: eris.New("root error"),
input: []string{"additional context", "even more context"},
output: "root error: additional context: even more context",
output: "even more context: additional context: root error",
},
"standard error wrapping with external root cause (errors.New)": {
cause: errors.New("external error"),
input: []string{"additional context", "even more context"},
output: "external error: additional context: even more context",
output: "even more context: additional context: external error",
},
}

Expand Down
54 changes: 33 additions & 21 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ func ExampleToJSON_global() {
// "root": {
// "message": "unexpected EOF",
// "stack": [
// "main.readFile:.../example/main.go:6",
// "main.parseFile:.../example/main.go:12",
// "main.main:.../example/main.go:20",
// "main.parseFile:.../example/main.go:12",
// "main.readFile:.../example/main.go:6"
// ]
// },
// "wrap": [
Expand Down Expand Up @@ -140,23 +140,23 @@ func ExampleToJSON_local() {
// "root": {
// "message": "unexpected EOF",
// "stack": [
// "main.readFile:.../example/main.go:3",
// "main.parseFile:.../example/main.go:9",
// "main.parseFile:.../example/main.go:11",
// "main.processFile:.../example/main.go:19",
// "main.printFile:.../example/main.go:29",
// "main.printFile:.../example/main.go:31",
// "main.main:.../example/main.go:37",
// "main.printFile:.../example/main.go:31",
// "main.printFile:.../example/main.go:29",
// "main.processFile:.../example/main.go:19",
// "main.parseFile:.../example/main.go:11",
// "main.parseFile:.../example/main.go:9",
// "main.readFile:.../example/main.go:3"
// ]
// },
// "wrap": [
// {
// "message": "error reading file 'example.json'",
// "stack": "main.parseFile: .../example/main.go: 11"
// },
// {
// "message": "error printing file 'example.json'",
// "stack": "main.printFile:.../example/main.go:31"
// },
// {
// "message": "error reading file 'example.json'",
// "stack": "main.parseFile: .../example/main.go: 11"
// }
// ]
// }
Expand Down Expand Up @@ -210,8 +210,18 @@ func ExampleToString_global() {
return nil
}

// call parseFile and catch the error
err := parseFile("example.json") // line 20
// example func that just catches and returns an error
processFile := func(fname string) error {
// parse the file
err := parseFile(fname) // line 22
if err != nil {
return eris.Wrapf(err, "error processing file '%v'", fname) // line 24
}
return nil
}

// call processFile and catch the error
err := processFile("example.json") // line 30

// print the error via fmt.Printf
fmt.Printf("%v\n", err) // %v: omit stack trace
Expand All @@ -223,11 +233,13 @@ func ExampleToString_global() {
fmt.Printf("%v\n", eris.ToString(err, true)) // true: include stack trace

// example output:
// unexpected EOF
// error reading file 'example.json'
// main.readFile:.../example/main.go:6
// unexpected EOF
// main.main:.../example/main.go:30
// main.processFile:.../example/main.go:24
// main.processFile:.../example/main.go:22
// main.parseFile:.../example/main.go:12
// main.main:.../example/main.go:20
// error reading file 'example.json'
// main.readFile:.../example/main.go:6
}

Expand Down Expand Up @@ -270,13 +282,13 @@ func ExampleToString_local() {
fmt.Println(eris.ToString(err, true)) // true: include stack trace

// example output:
// unexpected EOF
// main.readFile:.../example/main.go:3
// main.parseFile:.../example/main.go:9
// error reading file 'example.json'
// main.parseFile:.../example/main.go:11
// unexpected EOF
// main.main:.../example/main.go:17
// error reading file 'example.json'
// main.parseFile:.../example/main.go:11
// main.parseFile:.../example/main.go:9
// main.readFile:.../example/main.go:3
}

func TestExampleToString_local(t *testing.T) {
Expand Down
Loading

0 comments on commit bd2da70

Please sign in to comment.