How to know variable name adhering to go error - go

When working with go there is a pattern used to define errors and handle them in a (to me) very peculiar way. Often errors are declared like ErrorSomethingWentWrong = errors.New("Just an example!"). You can use errors.Is(err, ErrorSomethingWentWrong) to catch that specific error. The Is function can do this by comparing the pointers. But in order to make the comparison I need to know which variable name is used to define the errorString so I can use errors.Is to catch it.
For example:
ErrorSomethingWentWrong = errors.New("Just an example!")
func DoSomething() (*Something, error) {
return nil, ErrorSomethingWentWrong
}
I know a error is returned with the string "Just an example!" But I don't know it has the variable name ErrorSomethingWentWrong:
func checkError() {
if errors.Is(err, ErrorSomethingWentWrong){ // how to know this???
//handle error
}
}
When I use errors.Is(err, ErrorSomethingWentWrong) I can catch this error and handle it. When using debugging I can't see that the errorString represents the ErrorSomethingWentWrong variable. But when I don't know that the variable name was ErrorSomethingWentWrong I need to reverse engineer the code or read the docs to know which error is returned.
So how can you know using debugging or reflection to retrieve the error variable name?

If I understand correctly, this is your scenario:
You are receiving an error from a package, and there is no documentation to provide you with the errors you should be handling. You are debugging, and see an errors.errorString, but don't know if there is a global and public error value that you can compare to.
Unfortunately, this is a value and the debugger can't tell you if this is a global variable or if so which one, because you don't receive a variable, only a value.
You can:
read through the source code of the package you are calling and see which errors are being returned and how to handle them
search the source code for the text in the error string, this could help you pinpoint where the error is defined or created.
If it turns out the specific error you wish to handle is not defined globally (and re-writing the package is not feasible for your case), so you can't handle it. This can happen if somebody uses errors.New inside a function (bad practice). Then you can also try these:
func handleError(err error) {
if err.Error() == "full error string" {}
if strings.Contains(err.Error(), "partial error string") {}
}
But those are both ugly imho.
TL;DR It's not possible, you have to read the source code.

Related

Using an error variable as package-level vs global variable

Using the Go language I am writing a function that uses a custom error variable, e.g.
func readEnv() (map[string]string, error) {
var ErrConfig = errors.New("Configuration Error: Variables starting with the right prefix are not even")
if this {
return nil, ErrConfig
}
I declare this as a local var to avoid declaring it as a package-level var which is not recommended (and marked as issue from the linter if I am not mistaken)
The problem arises when I want to unit test this fxn and want to also test the error path (in which case the fxn should return the above error which however has not access to.). Now, the only way I can think of to address this is to re-declare this variable in my table test.
Which is the right way? Declaring ErrConfig as a package-level variable or just re-declaring it in the unit test fxn?
Does it matter to the caller of readEnv() what error you return?
If it doesn't matter, your test shouldn't care either, just check if the returned error is nil or not.
If it matters, in your current solution callers can't do any better than you in your tests. If it matters and clients should be able to tell, you have to export some kind of mechanism to test / examine the returned error.
One solution is to move ErrConfig to a package level variable. This is accepted, used numerous places in the standard lib too, e.g. io.EOF, io.ErrClosedPipe.
This is not the only solution though. You could make Errconfig unexported (e.g. errConfig), and provide an exported function to test an error, e.g.:
var errConfig = errors.New("Configuration Error: Variables starting...")
func IsErrConfig(err error ) bool {
return err == errConfig
}
This is also used in many places in the standard lib, e.g. os.IsExist(), os.IsNotExist()
You could also make the returned error implement an exported interface, and callers can check using a type assertion if the returned error implements that, providing extra functionality for the returned error.

How to suppress stack trace after `panic` in Go?

I am developing a CLI tool and in cases where the things go wrong, I want to log the custom error and exit with panic. The issue with panic is that an exit by panic is followed by the stack trace which I don't want to show to the user. Is there a way to panic and have a ninja-like stealthy/quiet exit?
(Choosing panic over os.Exit() since that would handle any defer and also seems a lot cleaner.)
Well, a direct answer is yes, there is a way:
panic with an error of a custom, "sentinel", type, or with a custom, "sentinel", value, then
Have a defer-red call in main which recover()-s the panic and checks whether the returned value is of a sentinel type (or equals to a sentinel value—the exact approach is up to you).
If it detects a "known" error, it silently exits with a non-zero exit code;
otherwise it panics again.
But honestly I think your mindset is too affected by programming languages with exceptions: I have yet to see a CLI app which would have any difficulty in handling errors "the usual way" or would actually benefit from bailing out using panic.
A counter-argument to the approach you're craving for is this: bubbling up an error allows adding more context to it on each level of the call stack being unwound, where it makes sense,—producing as useful as possible error to display.
Basically, it works like this:
func main() {
...
err := DoStuff()
if err != nil {
log.Fatal("failed to do stuff: ", err)
}
...
}
func DoStuff() error {
foo, err := InitializeWhatever()
if err != nil {
return fmt.Errorf("failed to inialize whatever: %w", err)
}
...
return nil
}
func InitializeWhatever() (*Whatever, error) {
handle, err := OpenWhateverElse()
if err != nil {
return nil, fmt.Errorf("failed to open whatever else: %w", err)
}
...
return whatever, nil
}
…which would produce something like
failed to do stuff: failed to inialize whatever: failed to open whatever else: task failed successfully
…which makes it crystal clear which sequence of events led to the undesired outcome.
Sure, as usually, YMMV and no one except you knows your situation best, but still it's something to ponder.
And here's an assorted list of thoughts on what I've written above.
The approach with panicking with a known value / error of a known type is actually nothing new—for instance, see net/http.ErrAbortHandler.
The encoding/json package used to employ this approach for breaking out of multiple nested loops (has been reworked since then, though).
Still, some experts consider sentinel errors to be bad design and recommend instead asserting (using Go's type assertions) behaviour of the errors—for instance, you can look at how net.Error has Timeout and Temporary methods which allows to not have concrete exported types for temporary errors, and errors due to timeouts, and their combinations,—but instead have them all support a common set of methods which allow the callers to make sense of the error's nature.
Since 1.13, Go gained advanced support for "wrapping" errors—producing matryoshka-style error chains, and this allows an approach to error handling which sits somewhere between the two extremes above: one can create an error of a particular type at the place an error was first found, then wrap it in contexts as they bubble up the call stack, and then assert the type of the innermost error at the place which handles the error. A friendly explanation on how it works.
Well, and I'd recommend reading this for, I must admit, had you already done this, you'd probably not ask your question in the first place ;-)

Checking (value or type) against errors returned by fmt.Errorf

If a piece of code has its own error type as in
var ErrSomethingWentWrong = errors.New("Something went wrong"
I believe in my code I can do this
import github.com/therepo/theproject/thepackage/thatexportstheaboveerror
// code that might return the above error
if err == thatexportstheaboveerror.ErrSomethingWentWrong {
// handle the particular case
}
What happens when the error returned is via fmt.Errorf as in this case?
return fmt.Errorf("ssh: unable to authenticate, attempted methods %v, no supported methods remain", keys(tried))
How can value (or type or whatever) check/assertion should be performed go-idiomatically?
Is this the only way around it?
if err != nil {
if strings.Contains(err.Error(), "unable to authenticate") {
// handle the particular error
}
}
Since Go version 1.13, the errors package has included the concept of "wrapping" errors. A routine returning a "wrapped" error calls fmt.Errorf using the %w verb in the format, to wrap an inner error into an outer error. An outer error can then be tested to see if it contains a particular inner error.
Prior to that, xerrors provided the same general concept.
Both of these require that whoever produces the error value use the provided wrapping interface. The routine you are asking about—part of https://godoc.org/golang.org/x/crypto/ssh—does not do this. Some older code that doesn't wrap errors provides certain kind of error-testing functions. For instance, when os.Open returns an error, os.IsNotExist will tell you if that error is because the file does not exist.
Unfortunately this particular package has no such test, so you're pretty much stuck with what you've suggested (direct string inspection) if you really want to know, programmatically, that this error came from this particular source.

How can I use dynamic errors while retaining comparability for testing?

In go I often use
func MyFunc(s someInterface) error {
err := OtherFunc(s)
return fmt.Errorf("something wrong: %s", err)
}
So I lose the original error value, because I just take the error string and forge it into a new error. That is what I mean with dynamic errors.
Now consider a test for MyFunc():
func TestMyFunc(t *testing.T) {
s := mockSomeInterface()
testErr := MyFunc(s)
if testErr != interfaceSpecificErrorValue {
t.Errorf("fail")
}
}
What would I use for interfaceSpecificErrorValue (which is specific to someInterface in this example)? Or how could I make this testable?
I understand that I can solve this by defining all my possible errors beforehand and give them a constant value. What I am interested in is if there is another way to achieve this, because I like the hierarchical error messages that you can dynamically build up using fmt.Errorf("...: %s, err). There must be a good way to keep the error hierarchy without losing the original value.
(Comparing the output of the Error() method is a possibility, but not a good one.)
My preliminary answer to this is: In Go currently there is no canonical way to achieve nested errors with comparable error values.
After reading the offical documents and blog posts on error handling again, quite some source code from standard libraries, and finally this proposal: https://github.com/golang/proposal/blob/master/design/go2draft-error-values-overview.md, I decided I will keep my error types simple and static and wait for Go 2 to offer a better way.

Mapping external errors to domain errors in golang

I have a service type called ComputeService which implements certain domain logic. The service itself depends on implementation of an interface called Computer which has a method Computer.Compute(args...) (value, error). As shown, Compute itself might return certain errors.
ComputeService needs to send appropriate errors from a set of domain-errors with proper domain-error code so that translations can be done and also clients can handle errors appropriately.
My question is, should the Computer implementations be wrapping their failure in domain-errors or should ComputeService do this. If ComputeService is the one doing it, then it will have to know about different errors returned by different implementations of Computer interface which in my opinion breaks the abstraction. Both ways are demonstrated below:
package arithmetic
type Computer struct {
}
func (ac Computer) Compute(args ....) (value, error) {
// errors is a domain-errors package defined in compute service project
return errors.NewDivideByZero()
}
OR
package compute
type Service struct {
}
func (svc Service) Process(args...) error {
computer := findComputerImplementation(args...)
val, err := computer.Compute(args...)
if err != nil {
if err == arith.ErrDivideByZero {
// converting an arithmetic computer implementation
// specific error to domain error
return errors.NewDivideByZero()
} else if err == algebra.ErrInvalidCoEfficient {
// converting an algebraic computer implementation
// specific error to domain error
return errors.NewBadInput()
}
// some new implementation was used and we have no idea
// what errors it could be returning. so we have to send
// a internal server error equivalent here
return errors.NewInternalError()
}
}
Implementors of Computer should respond with the domain errors, since they're the closest ones to the action and best able to determine what an error is. Like you said, having that logic in ComputeService breaks the abstraction. If you need mapping code from specific Computer errors to domain errors, create wrapper structs that separate the main logic from that error wrapping code.
To keep internal error context, just embed the original error in the domain error and make IsSpecificDomainError helpers.
type MyDomainError struct {
Err error
}
func NewMyDomainErr(err error) error {
return &MyDomainError{err}
}
func IsMyDomainError(e error) bool {
_, ok := err.(*MyDomainError)
return ok
}
To keep internal error context, just embed the original error in the domain error
This can use Wrapping errors, which are on their way for Go 1.13 (Q4 2019), from issue 29934, as detailed here.
err.Is():
As Russ Cox mentions:
I think we all agree that strings.Contains(err.Error(), "not found") is fragile code.
I hope we also agree that we'd prefer to see code like errors.Is(err, os.ErrNotExist).
But the point is that in many cases, it is essential to future evolution of a package to keep callers from depending on a particular error result satisfying errors.Is(err, os.ErrNotExist), even if that is the underlying cause in today's result.
It's like looking at an unexported field or comparing error text - it's a detail that might change.
And while strings.Contains looks and is fragile, errors.Is does not look nor should be considered fragile.
If we are to avoid it being fragile, then we need to provide a way for packages to report detail without letting clients test for it. That way is errors that can't be unwrapped.
err.As():
var pe *os.PathError
if errors.As(err, &pe) {
use(pe)
}
%w:
func inner() error { return errors.New("inner error") }
func outer() error { return fmt.Errorf("outer error: %w", inner()) }
fmt.Fprintf("%+v", outer())
// outer error:
// /path/to/file.go:123
// - inner error:
// /path/to/file.go:122
The current status for Go 1.13:
Just stating what I see as the compromise solution offered by the team:
fmt.Errorf is currently being used extensively to wrap errors and return a new (opaque) error (as in you cannot access the underlying error).
'%w' can now be used to explicitly opt-in to return an error that can be unwrapped.
errors is designed as a base package with no dependencies so that every package can depend on it.
the team agrees to punt on the areas that there is broad disagreement, and want to release just enough (errors.Is, errors.As, extension to way most folks wrap errors) so folks can achieve things.
Generics is not here yet, and we do not know when it will come: the heated discussion on that will make this one on "error 2 values" look like child's play.
errors.Is and errors.As are clean and concise enough to be comfortable for a long time.
Most of the contentious things have been punted to go 1.14.
Wrapf cannot live in errors as it is a base package.
Wrapf means team MUST decide on what happens when a nil error is passed: Punt on it.
Wrap may conflict with ideas being considered for localization, internationalization, etc.
ErrorFormatter and ErrorPrinter have not yet gotten much deeper usage and have warts. Punt.

Resources