Working with Errors in Go 1.13
blog.golang.org
blog.golang.org
Rolling your own error library is trivial. We have a great one at work that works great, prints stack traces, interops with normal error handlers, and does a lot of custom work for translating errors into external customer-visible messages and internal developer messages. We use it at every layer of a 50-60 grpc microservice ecosystem without much headache.
Catching deferred errors is trivial, not sure what you mean there:
defer func () {
err := f.Close()
...
}()Printing stack traces is like 4 lines, I was able to implement ours in 60 minutes of doc skimming. Now it'd be 5 minutes.
I don't understand your last two sentences. It doesn't reflect anything I've run into in the real world, I don't think.
I have a few gripes about golang, but the minimalist error interface is not one of them.
https://play.golang.org/p/7Gs76Nt-h4b
That issue is one of the reasons everything has to return 'error', not your own struct that might be more convenient to work with. I've run into it in the wild in dozens of codebases, so it's definitely a real issue.
Without generics and covariance, it's an uphill battle to create usable monadic error handling in Go. If all you've used before is C which has int returns and globals for error handling, I can see how go's error handling looks nice, but compared to most languages invented in the last 20 years, it feels far worse.
Sounds like the C and C++ situation where each company and large project rolls out their own string implementation.
There are still stuff missing, but not basic low level stuff that everyone uses daily.
Minimalist standard library doesn't go well with their philosophy around dependency (“a little copying is better than a little dependency”). That's why the made so much stuff in std.
And right there is where you’re going to lose most people.
If it’s is trivial, why isn’t in the standard library? If everyone needs to do it, why not standardize? I love Go, but i have to agree with grandparent poster.
If people don't agree then it by definition does not belong in the stdlib.
For example, no one likes gofmt (tabs, eww), but everyone likes code across packages looking the same. And so we use it.
I guess I finished that sentence in a different way than I started it.
Getting a stack trace is available in the runtime/debug library.
Perhaps stack traces aren't in normal errors since a program can throw 1,000 errors per second in a perfectly functioning application and that could get expensive.
I have heard that performance is the reason but I'm not able to confirm that.
github.com/pkg/errors is fairly ubiquitous and includes stack traces.
> If everyone needs to do it, why not standardize
The only time I've needed to read the stack trace in go, is when there has been an unexpected panic. Otherwise my error messages are more than sufficient to find the root cause of the error. I only have to run my own code though, I imagine if I was debugging someone elses code the stacktrace would be invaluable.
Go has purposely tried to do a few things differently. Date format comes to mind, and lack of exceptions which I personally like.
Trivial does not mean generic. Just because error handling is easy doesn't mean I want the same handling as you do.
Error handling is generic by itself. This is at the heart of any existing application. It is fundamental part of the design process and later on contributes greatly to troubleshooting. Making this solid should be, IMHO, one of the most important and thought through part of any language. In Go it seems to be left at the developer's convenience. And even though you may say there is a huge debate on the subject, it always leads to nothing. Or almost nothing, like in this case. It's just disappointing.
When working in other programming languages, this is something I sorely miss and have to resort to ugly and non intuitive try catch constructs which feel like "bolt-on magic" rather than a natural part if the program.
IMO this feature makes up for most of the stuff that's lacking.
I'm not sure where you're going with the stack traces complainr. It is pretty trivial to get it in Go.
Treating errors as value is cool (no exception!) but using multiple values not so much, the proper way to do so is with product types, but Go's type system don't have them unfortunately…
I had a situation recently where a fairly complicated API was returning an HTTP 403 in production. Once the error bubbled down to a level where it was actually logged, all that was left was "403 Forbidden". I spent days echo debugging and could not reproduce the problem. Finally, by chance, I happened on the problem while working on something completely unrelated. With a stack trace, 3 days of work would have taken 5 minutes. Wrapping would not help in this situation unless every single error was wrapped with a unique string, which frankly is ridiculous.
But I’ll see Java programs that give me stack traces just because I’ve misconfigured them, or the server is down, or I have a bad network connection, or a file is missing. This is awful UX. Worse, the stack trace might not tell me WHICH file is missing, or WHICH server is down.
You only get good UX for errors by manually annotating context in the places in your code where the context is known.
(My personal experience is that FAR too many actual Java programs in the wild will give me a stack trace when they should give a short error message.)
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.
By the way what's left of all the "Go 2" proposals? "generics" look like they are in limbo and so is the "try/catch" proposal. Has anything been implemented yet?
They don't want to do generics, they never have.
The described changes could have been added by anyone in earlier Go versions. These are not things which can be only implemented by the language or compiler implementor, these are just APIs, which are standardized.
My thoughts on Go error handling has made me conclude that there is probably no really good way to do it. Every approach will come with a load of downsides.
I love how error handling is done in Rust. Yes, implementing the From<T> trait can get verbose, but wrapper libraries like Failure or Snafu exist now as well.
Being able to propagate errors down the line seamlessly with the ? operator and Result<T, E> type is nothing short of phenomenal. It looks like the next version of Rust will seamlessly allow using ? on None types as well.
For lots of errors, just treating them as a generic failure makes code a lot better.
I don't like having to put decorators on functions and manually throw!()'ing. I would rather be explicit about returning Ok(T) or Err(E).
1) The state of "how best to handle errors" keeps on changing. It was error_chain, then err-derive, then Failure, now Fehler or Snafu.. I would like to see this space calm down a bit.
2) Failure/Snafu require additional dependencies. For minimal libraries it's really not necessary to pull in so much when a a little boilerplate suffices. [0]
[0]: https://git.sr.ht/~andrewzah/hangeul-rs/tree/master/src/erro...
Has it? As a Java developer who needed to write something in Go, this has been quite a frustration for me, personally. In Java, when I get a 100-line stacktrace error, my IDE analyzes it for me and I can click through the code and follow what's happening. In Go, I get a string from the developer. Hmm...
To make it cleaner, he wanted to absorb the whole stack for any error raised in our library and only print the error text. To make them more useful and helpful, he wanted us also include some guides on how to debug and solve the problem, based on the error, in the error text.
Fortunately I was able to convince him this would make things difficult for everyone, in a calm collective way.
Step 2) Hire a lot of yes men
At least in OPs response, their manager defers to those who know better.
I think that's the parent's point.
Though, not that that is bad Go API design on behalf of the library authors (as you imply), but that that is a sign of Go's own bad design -- the fact that it leaves it to programmers own devices to get their API design right with respect to errors...
AKA roll your own stack trace library. That's what "errors" package and co are. That's not "bad God design". It's just not Go's problem, as deemed by Go designers.
Thus criticizing that trade-off is fair, like any trade-offs.
Most people who were sceptical about Go or were vocal about trying to improve the language moved on to something else, because they faced a wall of contempt. The whole Go 2 stunt is more an "idea parking lot" than anything else.
The interesting thing about the new error APIs is, they are just auxiliary APIs. Every user could have written them, they are not mandatory to be in the core language or even in the compiler. They are just added as a standardized guideline how common tasks are handled and especially are standardized across packages from different maintainers, if they decide to follow that convention.
The Java equivalent would be a specific FileNotFoundException with a text mentioning a file name, versus the general class hierarchy of FileNotFoundException -> IOException -> Exception which is routinely used for "Is" tests in Java.
Oh, and if that hope was wrong in Java? Thread does, nice call stack in logs. In Go? Well, the program will go on with some default - value struct, most likely.
Some people would argue that this is what linters do: warn you when you don't check errors as return values. I'm not sure what's the best solution here, and I don't think any language handles this out of the box (without linters)
> and if that hope was wrong in Java?
Denial of service
But I get your point though, errors are hard and I think the only language that I've seen doing something good with them is Erlang. You just expect errors to crash your actors, and you design to recover quickly from any type of crash.
Maybe something like this could be supported natively in Golang:
func thing(arg bool) Error.Result(bool, error) {
if arg {
return Error.Ok(false)
}
return Error.Error(fmt.Errof("nope"))
}
func main() {
if thing() == true {} // doesn't compile
match thing(false) {
Error.Ok(value) => {
fmt.Println(value)
},
Error.Error(err) => {
log.Panic(err)
}
}
}
just for fun I wrote an ugly PoC for bool options: https://github.com/mimoo/BoolAlternatively, it looks like there are libraries out there[0] that will include stack traces for you. It seems weird to me that it's necessary to use an external library to get what seems like it should be built into the language.
Can anyone enlighten me?
Edit: Forgot to include the reference:
Rob Pike has this to say:
Our proposal instead ties the handling to a function - a dying function - and thereby, deliberately, makes it harder to use. We want you think of panics as, well, panics! They are rare events that very few functions should ever need to think about. If you want to protect your code, one or two recover calls should do it for the whole program. If you're already worrying about discriminating different kinds of panics, you've lost sight of the ball.
Your top level comment about 403 errors made me assume you did not know about panic. Obviously your particular situation may have prevented it but I've always built Go programs from source (not linking binaries) which means I could always panic whenever I needed a stack trace.
github.com/pkg/errors is the ubiquitous library for seamlessly including stacktraces in errors
I maintain an error package that lets users use structured types for errors and error codes [1]. It is critical to be able to wrap errors without information loss. I used one standard, a Causer interface, but I will be switching to Unwrap.
I see complaints about stacktraces here in the comments. I recommend always using pkg/errors or some error package with stack traces.
Finally Go gets it ahead of the other languages!
[1] https://go.googlesource.com/proposal/+/master/design/29934-e...
[2] https://github.com/golang/go/issues/29934