Is this really the best we can do?
if err := frobulate(); err != nil {
log.Printf("Error on line 123: %+v", err)
return err
}When dealing with error handling in Go (and many other languages), you should do one _and only one_ of the following:
- if you have enough context to handle the error (eg. retry, or degrade gracefully, or perform whatever other application logic to recover, possibly returning a more friendly error to your caller), do that, possibly logging the source error and do not return it further up the stack directly - you have now healed the error condition, and the application code can continue.
- otherwise, return the error, decorating it with extra context if possible.
Doing both, ie. logging (which is a case of handling the error) and returning it up the stack leads to extremely chatty and difficult to debug code. Errors are a fact of life, and your path of handling them should be as well engineered as the happy life. That's one of the most important things in writing reliable software.
That's also one of the reasons there's no generic stack unwinding/exceptions on Go: it forces you to _think_ about error cases and at least reminds you to handle them gracefully. You might not like this approach of treating programmers this way, but that's one of the cornerstones of Go.
> - otherwise, return the error, decorating it with extra context if possible.
This is precisely how checked exception handling works in Java, and yet there's no shortage of people complaining about it and praising how Go "makes you deal with it the way you're supposed to."
> it forces you to _think_ about error cases and at least reminds you to handle them gracefully.
So do checked exceptions. And 99% of the time, the solution to an error popping up in Java is to blindly re-throw it because hey, I'm just a component buried a couple levels deep in a REST call, what could I possibly do to fix the problem?
> This is precisely how checked exception handling works in Java, and yet there's no shortage of people complaining about it and praising how Go "makes you deal with it the way you're supposed to."
Just because people complain about Java’s checked exceptions does’t mean that their complaints are correct or that they somehow apply to Go’s errors.
The main complaints I hear about checked exceptions are that they force you to expose the types of all possible exceptions in the method signature. This creates unexpected API churn and design difficulties, because it’s difficult to predict which exception types you would want to expose from a particular interface.
You also have to choose which errors are programming errors and which are not when you declare the error type. This turned out to cause problems. NumberFormatException is an example.
> So do checked exceptions. And 99% of the time, the solution to an error popping up in Java is to blindly re-throw it because hey, I'm just a component buried a couple levels deep in a REST call, what could I possibly do to fix the problem?
In Go, blindly returning an error is done much less than 99% of the time, at least in the code bases I’ve worked on or read.
1. Open a file for reading
2. Try to open a file for writing, but error
3. Close the file that was opened for reading
4. Propagate the error upwards (because the reason this function was called failed)This is the pattern I usually see in actual Go codebases:
infp, err := os.Open(infile)
if err != nil {
return err
}
defer infp.Close()
outfp, err := os.Create(outfile)
if err != nil {
return err
}
defer outfp.Close()
// ... do some stuff ...
return outfp.Close()
Note that double-close is explicitly OK, this pattern just avoids losing the error.The double-close pattern is just the easy way to accomplish this in go. You defer fp.Close(), and then return fp.Close(). The return fp.Close() closes the file and propagates the error. Afterwards, the deferred fp.Close() executes, but this simply returns an error (which is ignored) because the file is already closed.
The behavior of fp.Close(), the fact that it is safe to call multiple times, is documented in the Go documentation.
There are alternative ways to achieve the same effect but they require more code. So I use the double-close pattern everywhere, and then put a comment in every time I use it.
Like everything there are exceptions to the rule and at times it makes sense to do both, but it shouldn't be the norm.
1. If you can handle it, you probably should also log it since you know what it means and what's being done about it.
2. Otherwise, if you can add pertinent information, like for example the fact that the file you couldn't find was supposed to be the SQLite database, but can't handle it here, wrap and return. Do not log.
3. Otherwise, sticky fingers off, pass it up unaltered. Do not log.
Which means you need to abort when you find an error condition, and roll back everything, all the way up the stack.
The more stateful your problem domain, the more likely you need recovery logic, and error codes start becoming more useful. Add in nondeterminism and error conditions are non-exceptional and exceptions are often not the best model for them.
You don't have to think in N-layers-deep call stacks, that's a Java way of thinking. Log the problem in sufficient detail where you found it, and stop.
This is probably a use case for Go's panic and recover mechanism, where you actually do want to blow away a whole piece of the running code.
Stacktraces are probably the single most useful tool in debugging any issue and their absence in Go is one of my only enduring gripes with the language.