6 ms·
Okay friends, the reason why Go’s errors are good as-is is because you actually have to think about them. That is a feature. If you stop thinking about them and
by teeray 4y ago
Okay friends, the reason why Go’s errors are good as-is is because you actually have to think about them. That is a feature. If you stop thinking about them and live in happy-path land forever, you will forget that bad things can (and often do) happen during the execution of your code. Even if you just return the error up the call stack, you’re still making a decision about it. You have an opportunity to examine the error at that point and even wrap it with additional context based on what you know at that point in the call stack. The exceptional path of the code is constantly maintained instead of handed to a runtime with the hopes it will do the right thing (particularly with things like retries).
- 015a 4y agoThat's fair, but I don't feel that Go's position of biasing to the absolute opposite end of the spectrum is reasonable. Ultimately; in a typical app; if you've got a phrase of code like: result, err := db.GetUser("12345") if err != nil { return nil, fmt.Errorf("failed to query user: %v", err) } There's an inherent asymmetry between how often those three conditional lines are executed relative to the undeniable fact that error handling is three times the code line volume versus the core business logic. That's not ok; and when I look at the argument phrased like that I find it hard to believe that anyone reasonable can take the stance that its desirable that error handling requires writing substantially more code than The Thing Which Generated The Error In The First Place.
- svnpenn 4y ago> There's an inherent asymmetry No there's not. People seem to just want to ignore all errors, and let the language "figure it out", AKA exceptions. If you want to ignore errors, then just go all the way and do that: result, _ := db.GetUser("12345") then, you never have to worry about errors, you can just code in bliss and wait for the "panic" because you didn't want to bother with proper error handling.
- peterashford 4y agoThat really misrepresents how exceptions are used, IMO.
- sidlls 4y agoDepends on the code base, as usual, and the quality of the development practices the code was written in. Same as go's error handling, actually.
- xigoi 4y agoWith exceptions, you can handle the error in the place that can handle it. That's not the case with your example.
- bheadmaster 4y agoActually, your example would continue running the happy path as if the error didn't happen, returning erroneous results, but not panicking: func main() { x, _ := getNonZero() fmt.Println("Nonzero number:", x) } func getNonZero() (int, error) { return 0, errors.New("oops") } The "_" is supposed to be used only when you're absolutely sure that error cannot happen (e.g. the function you're calling is partial, but you know that the input is in the defined set).
- Arbortheus 4y agoI disagree, I think that handling the failure cases properly is much more complex and requires more consideration than the success cases. Sure, in many cases you’re going to bubble up the error (perhaps a syntactic sugar for this scenario would help), but there are lots of production error handling considerations like transaction rollbacks, response statuses, logging, metrics, opentelemetry traces, retry logic, that you will need to handle in practice, and you will want to handle these differently in different failure modes. Ensuring software acts in an expected way in the transient 0.001% scenarios is very important. With Go I never run into unhandled exceptions causing software to blow up like I do with large Python codebases. At first I didn’t like the “if err != nil” bloat of Go code, but now I appreciate its explicitness, it is distinctly unmagical, simple, boring, and works well in production.
- 015a 4y agoThis isn't a situation where the choices are: 3 lines of IDE-Insert-Boilerplate, or Exceptions. I think Go could add syntax to keep its relatively unique form of error handling, while reducing code noise. The guard proposal is fine, but I'd prefer to see something like: func MyFunc() error { r := db.Get("12345") guard "error Get(12345)" } Syntactically really strange, but point being: if a function returns (any, ..., error); 'guard' is a keyword which eats the last 'error', optionally prefixes it with the string in that last keyword argument (standard golang error wrapping: "error Get(12345): {err.Error()}", and bubbles it up (along with zero or default values for the other arguments). It would also be nice, for more complex use-cases, if that last argument could be a mapping function as well, like: func MyFunc() CustomError { r := db.Get("12345") guard func(err error) CustomError { return CustomError{baseErr: err, code: 500} } } Though, I'd gladly settle to not have that, as it goes a little beyond the extreme syntactic simplicity I expect out of go.
- klodolph 4y ago> [...] I find it hard to believe that anyone reasonable can take the stance that its desirable that error handling requires writing substantially more code than The Thing Which Generated The Error In The First Place. I have personal experience with Java, Go, and C#, to varying degrees. Over the years, my personal style has changed a lot, based on my experience running applications (as someone in ops), developing applications, debugging applications, etc. First of all, I'll say that doing Go error handling well figuring out the right place to add context to your errors. So in the example above: return nil, fmt.Errorf("failed to query user: %v", err) The error is from a method named "GetUser", and in the style guide I use, errors from GetUser would already say something like "get user %d: %w". You'd know this when writing code that calls GetUser, and wouldn't double-wrap it with additional context, if the additional context is not useful. That's a bit of a tangent. My experience working in operations is that errors in languages like Java are often stack traces. The stack traces are often large and often have poor signal-noise. Important context is often missing--things like arguments to methods. C++ is sometimes worse, since there's a lot of C++ code out there which won't even give you a stack trace. Worst of all worlds, perhaps. As I've gotten older, I've moved to a very proactive style of error checking. This style is verbose in all languages, and sometimes this style is much more natural in Go. That's because of the number of times that putting effort into error checking has saved my butt when debugging. By the time I'm debugging a piece of code, it might long forgotten. The error might be hard to reproduce. Getting a good report the first time saves a ton of time when debugging. In other languages, this might be accomplished by aggressive logging, or tools like time travel debugging, or catch & rethrow. My sense of Go's error handling is that it is a lot of code only if you measure it by line count. The error handling is very boilerplate and you stop seeing it, like when Cypher's looking at code in The Matrix. https://www.youtube.com/watch?v=MvEXkd3O2ow https://www.youtube.com/watch?v=MvEXkd3O2ow In isolated cases, it makes sense to panic() / recover() in Go. People new to Go sometimes overuse panic(), and then once they get more experience they overcompensate and don't ever panic()... I think there's a middle ground here, where panic()/recover() is useful in certain situations. I use it most for parsing / marshaling / unmarshaling code. Parsers, for example, can be very dense, and the error handling is often just "return to top, throw everything away, no additional context to report". What we need is some education like "case studies for when you might want to panic/recover in Go" or "case studies in where you should add context to error messages".
- Gibbon1 4y agoI've sometime wonder about passing a label for error handling result, err := db.GetUser("12345": no_user) no_user: return nil, fmt.Errorf("failed to query user: %v", err) If there is an error the program jumps to the label. If no label the program panics. This implied that you have both return and return_error keywords.
- IshKebab 4y ago> error handling is three times the code line volume versus the core business logic. That's not ok It is ok. That's really how it should be IMO. The "problem" is that Go doesn't allow you to say "eh, we'll just give a stack trace to the user, and then they can delete it print Oops something went wrong" which is apparently what most people want from their error handling.
- vlovich123 4y agoI agree with the theoretical aspect of that position, except from what I’ve seen no one ends up testing the error paths. Untested error handling can have serious bugs that negate the whole value prop, so in practice the value is not as large as claimed. Also, I think the other critique is less about forcing you and more about the syntax overload your force onto someone. Rust and swift come to mind as counter examples where you’re forced to handle every error AND the language tries to keep that out of normal code flow unless you are actually having to make some decision regarding the specific error.
- jayd16 4y agoCan you explain why this is better than checked exceptions (but rethrow is 3 lines)?
- peterashford 4y agoAs someone who really likes Go, I don't agree with you on this one. To me, it just means writing a lot of pointless boiler code and what's really worse is that the "happy path" of the code is obscured underneath a pile of pedantic make-work code
- grey-area 4y agoYes the fact the Go makes you use errors is good. The particular syntax for returning an undecorated error is needlessly verbose and takes three lines. We shouldn’t have to write this say three times in a function: if err != nil { return nil, 0, err }
- kaba0 4y agoAs another commenter mentioned, do you really think about them, or just tell yourself that you think about them while writing the same boilerplate millions of times?
- topspin 4y agoIndeed. Also, this notion that Go makes one think about all the errors is false. What happens to the errors from all the "defer foo.CloseOrMaybeFailSomehow()" that get run on early return? Answer: they're ignored. Ignoring errors baked right in. Yes, you can handle them with a closure, but I can count on part of one hand the number of times I've actually seen that. And I know why: because at some point the regression gets unreasonable and people just pretend they didn't see it and thank their makers the compiler lets them get away with it, this blog post in question being a fine example of that. So I know what sort of malarkey I'm reading when this "think about the errors" gets bandied about.
- topspin 4y ago> because you actually have to think about them. Ok friend, but right here in this blog post is contrary evidence. The supposed "actual CopyFile function" ignores errors from two deferred .Close()s at the io.Copy() call site. [1] And this is idiomatic Go. It goes on every day and you can find thousands of examples of it all over GitHub et al. Solving for it quickly leads to a real eye-sore: a deferred closure or similar blight. So if "think about them" is your aspiration then Go has failed. [1] https://pkg.go.dev/os#File.Close https://pkg.go.dev/os#File.Close