Skip to content

Commit

Permalink
feat: try to unwrap external errors during error wrapping
Browse files Browse the repository at this point in the history
  • Loading branch information
morningvera committed Feb 21, 2020
1 parent 1a1f12c commit 8fa9332
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 13 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ runtime.goexit

Migrating to `eris` should be a very simple process. If it doesn't offer something that you currently use from existing error packages, feel free to submit an issue to us. If you don't want to refactor all of your error handling yet, `eris` should work relatively seamlessly with your existing error types. Please submit an issue if this isn't the case for some reason.

Many of your dependencies will likely still use [pkg/errors](https://github.com/pkg/errors) for error handling. Currently, when external error types are wrapped with additional context, the original error is flattened (via `err.Error()`) and used to create a root error. This adds a stack trace for the error and allows it to function more seamlessly with the rest of the `eris` package. However, we're looking into potentially integrating with other error packages to unwrap and format external errors.
Many of your dependencies will likely still use [pkg/errors](https://github.com/pkg/errors) for error handling. When external types are wrapped with additional context, `eris` attempts to unwrap them to build a new error chain. If an error type doesn't implement the unwrapping interface, the original error is flattened (via `err.Error()`) and used to create a new root error instead.

## Contributing

Expand Down
28 changes: 25 additions & 3 deletions eris.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ func Errorf(format string, args ...interface{}) error {
//
// This method behaves differently for each error type. For root errors, the stack trace is reset to the current
// callers which ensures traces are correct when using global/sentinel error values. Wrapped error types are simply
// wrapped with the new context. For external types (i.e. something other than root or wrap errors), a new root
// error is created for the original error and then it's wrapped with the additional context.
// wrapped with the new context. For external types (i.e. something other than root or wrap errors), this method
// attempts to unwrap them while building a new error chain. If an external type does not implement the unwrap
// interface, it flattens the error and creates a new root error from it before wrapping with the additional
// context.
func Wrap(err error, msg string) error {
return wrap(err, msg)
}
Expand Down Expand Up @@ -72,10 +74,30 @@ func wrap(err error, msg string) error {
root.stack.insertPC(*stack)
}
default:
// attempt to unwrap external errors while building a new error chain or fallback to flattening them
var errStr []string
for e != nil {
errStr = append([]string{e.Error()}, errStr...)
e = Unwrap(e)
}
err = &rootError{
msg: e.Error(),
msg: errStr[0],
stack: stack,
}
for i := 1; i < len(errStr); i++ {
// parse the current layer's message by substracting the other layers
// note: this assumes delimiters are two characters like ": "
var layerMsg string
msgCutoff := len(errStr[i]) - len(errStr[i-1]) - 2
if msgCutoff >= 0 {
layerMsg = errStr[i][:msgCutoff]
}
err = &wrapError{
msg: layerMsg,
err: err,
frame: frame,
}
}
}

return &wrapError{
Expand Down
74 changes: 65 additions & 9 deletions eris_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,10 @@ func TestErrorWrapping(t *testing.T) {
input: []string{"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: "even more context: additional context: external error",
},
"no error wrapping with a local root cause (eris.Errorf)": {
cause: eris.Errorf("%v root error", "formatted"),
output: "formatted root error",
},
"no error wrapping with a third-party root cause (errors.New)": {
cause: errors.New("external error"),
output: "external error",
},
}

for desc, tc := range tests {
Expand All @@ -85,6 +76,71 @@ func TestErrorWrapping(t *testing.T) {
}
}

func TestExternalErrorWrapping(t *testing.T) {
tests := map[string]struct {
cause error // root error
input []string // input for error wrapping
output error // expected output
}{
"no error wrapping with a third-party root cause (errors.New)": {
cause: errors.New("external error"),
output: eris.New("external 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: eris.Wrap(eris.Wrap(eris.New("external error"), "additional context"), "even more context"),
},
"wrapping a wrapped third-party root cause (errors.New and fmt.Errorf)": {
cause: fmt.Errorf("additional context: %w", errors.New("external error")),
input: []string{"even more context"},
output: eris.Wrap(eris.Wrap(eris.New("external error"), "additional context"), "even more context"),
},
"wrapping a wrapped third-party root cause (multiple layers)": {
cause: fmt.Errorf("even more context: %w", fmt.Errorf("additional context: %w", errors.New("external error"))),
input: []string{"way too much context"},
output: eris.Wrap(eris.Wrap(eris.Wrap(eris.New("external error"), "additional context"), "even more context"), "way too much context"),
},
"wrapping a wrapped third-party root cause that contains an empty layer": {
cause: fmt.Errorf(": %w", errors.New("external error")),
input: []string{"even more context"},
output: eris.Wrap(eris.Wrap(eris.New("external error"), ""), "even more context"),
},
"wrapping a wrapped third-party root cause that contains an empty layer without a delimiter": {
cause: fmt.Errorf("%w", errors.New("external error")),
input: []string{"even more context"},
output: eris.Wrap(eris.Wrap(eris.New("external error"), ""), "even more context"),
},
}

for desc, tc := range tests {
t.Run(desc, func(t *testing.T) {
err := setupTestCase(false, tc.cause, tc.input)

// unwrap the actual and expected output to make sure external errors are actually wrapped properly
var inputErr, outputErr []string
for err != nil {
inputErr = append(inputErr, err.Error())
err = eris.Unwrap(err)
}
for tc.output != nil {
outputErr = append(outputErr, tc.output.Error())
tc.output = eris.Unwrap(tc.output)
}

// compare each layer of the actual and expected output
if len(inputErr) != len(outputErr) {
t.Fatalf("%v: expected output to have '%v' layers but got '%v': { %#v } got { %#v }", desc, len(outputErr), len(inputErr), outputErr, inputErr)
}
for i := 0; i < len(inputErr); i++ {
if inputErr[i] != outputErr[i] {
t.Errorf("%v: expected { %#v } got { %#v }", desc, inputErr[i], outputErr[i])
}
}
})
}
}

func TestErrorUnwrap(t *testing.T) {
tests := map[string]struct {
cause error // root error
Expand Down

0 comments on commit 8fa9332

Please sign in to comment.