"The traditional error handling idiom in Go is roughly akin to .... which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error."
Even if you generate a stack trace at the source, callers of a function (which returns an error) will in general want to wrap it again to add more context, and the preferred method is to use errors.Wrap(), not errors.WithMessage().
, err := externalFunction(r)
if err != nil {
return errors.Wrap(err, "msg") // or should we .WithMessage??
}
Perhaps there could be a more intelligent automatic way to choose between errors.Wrap() or errors.WithMessage(), but what would that look like? The only obvious 100%-correct-general-solution given the state of github.com/pkg/errors today (without assuming anything about the implementation of externalFunction() error) is to always use errors.WithMessage() but include minimal trace information like the filename and line number.
So OK, if github.com/pkg/errors starts advertising errors.WithMessage() as the primary method of adding contextual information, and its implementation is changed to include a modicum of trace information (filename, lineno), (or if errors.Wrap() were changed to include only minimal trace information), then we're 89% of the way there. But there's still a problem.
In general, it breaks the intent of Golang to wrap an error with github.com/pkg/errors.Error, because it breaks the one way that we're supposed to deal with error control flow... that is, we're supposed to switch on err behavior (or err concrete type, though it's not preferred).
type FooError struct{}
func (_ FooError) Error() string { return "" }
type BarError error
func externalFunc() error { return nil }
func main() {
err := externalFunc()
switch err.(type) {
case FooError:
// handle FooError
case BarError:
// handle BarError
}
fmt.Println("Hello, playground")
}
And github.com/pkg/errors violates that. There exists errors.Cause(), but now you have to dig down to the first non-errors.Error-error before you can type-check like above. And you can't just take the root-most cause of an error unless you preclude other custom error types from having a cause.
The solution is to not wrap an error, but to add trace information to the same error object. Ultimately it is error itself that needs to be a tracer.
Partial solution
If github.com/pkg/errors were to always keep a single (non-nested) Error by returning the same github.com/pkg/errors.Error upon errors.Wrap() and errors.WithMessage(), then we can switch confidently on behavior:
var err error := externalFunc()
switch err := errors.Unwrap(err).(type) {
case FooError:
// handle FooError
case BarError:
// handle BarError
}
The name for errors.Unwrap() is tricky... It can't be errors.Cause() because if it were a non-github.com/pkg/errors.Error causer error, we'd be switching on the wrong error. So Unwrap seems like a better name.
The problem is that it isn't 100% consistent with errors.Wrap(), because you only need to unwrap once what was wrapped a million times. Maybe errors.Wrap() should be deprecated, and errors.TraceWithError(error) error could ensure that the error err is already a errors.Error (in which case it would pass it through after calling err.Trace()) or else it would return errors.Wrap(err). Maybe errors.Unwrap() should be called errors.MaybeUnwrap().
Original discussion: golang/go#23271 (comment)
Even if you generate a stack trace at the source, callers of a function (which returns an error) will in general want to wrap it again to add more context, and the preferred method is to use
errors.Wrap(), noterrors.WithMessage().Perhaps there could be a more intelligent automatic way to choose between
errors.Wrap()orerrors.WithMessage(), but what would that look like? The only obvious 100%-correct-general-solution given the state ofgithub.com/pkg/errorstoday (without assuming anything about the implementation ofexternalFunction() error) is to always useerrors.WithMessage()but include minimal trace information like the filename and line number.So OK, if
github.com/pkg/errorsstarts advertisingerrors.WithMessage()as the primary method of adding contextual information, and its implementation is changed to include a modicum of trace information (filename, lineno), (or iferrors.Wrap()were changed to include only minimal trace information), then we're 89% of the way there. But there's still a problem.In general, it breaks the intent of Golang to wrap an error with
github.com/pkg/errors.Error, because it breaks the one way that we're supposed to deal with error control flow... that is, we're supposed to switch onerrbehavior (orerrconcrete type, though it's not preferred).And
github.com/pkg/errorsviolates that. There existserrors.Cause(), but now you have to dig down to the first non-errors.Error-error before you can type-check like above. And you can't just take the root-most cause of an error unless you preclude other custom error types from having a cause.The solution is to not wrap an error, but to add trace information to the same error object. Ultimately it is
erroritself that needs to be a tracer.Partial solution
If
github.com/pkg/errorswere to always keep a single (non-nested)Errorby returning the samegithub.com/pkg/errors.Erroruponerrors.Wrap()anderrors.WithMessage(), then we can switch confidently on behavior:The name for
errors.Unwrap()is tricky... It can't beerrors.Cause()because if it were a non-github.com/pkg/errors.Errorcauser error, we'd be switching on the wrong error. SoUnwrapseems like a better name.The problem is that it isn't 100% consistent with
errors.Wrap(), because you only need to unwrap once what was wrapped a million times. Maybeerrors.Wrap()should be deprecated, anderrors.TraceWithError(error) errorcould ensure that the errorerris already aerrors.Error(in which case it would pass it through after callingerr.Trace()) or else it would returnerrors.Wrap(err). Maybeerrors.Unwrap()should be callederrors.MaybeUnwrap().Original discussion: golang/go#23271 (comment)