counts as a legitimate error handling strategy, you either have an extremely low bar set or haven't used Go much. Then again, people seem to be okay with Golang allowing nil pointer exceptions to exist in 2021, so maybe the bar is underground.
Using product types instead of sum types to represent "success or error" is definitely something wrong with it. Product types should be used for "and", and sums should be used for "or".
Because when you return `foo, err` , your caller has to remember not to use the foo.
And when you're constructing your return values, often if you have an error, you won't have a real `foo` to give back, which raises the temptation to return a pointer-to-foo instead so you can give nil in that case.
But then your caller has to check for the nonsense case where there's no error but also a nil foo.
Usually what is meant is "I'll return a foo _or_ an error" but what go's type system encodes is "I'll give you a foo and maybe an error" or "I'll give you maybe a foo and maybe an error" depending on what choice you make.
Because the typical situation with error handling is that there are two possible cases: (a) a function succeeds with some result, or (b) it fails with an error. A sum type represents those two possibilities exactly. In contrast, a product type says "I have both a result and an error", which makes no sense. However, if things can be nil, which is the norm in Go, then you can represent both (a) and (b) with a product type, but you also have two additional cases to worry about: (c) both the result and the error are present, and (d) both are nil (and what are you supposed to do in that situation?).
When using static types, it is best to make as few nonsensical/illegal/invalid states representable as possible, so that you can rely on the compiler enforcing that they won't happen. Functional programmers have been saying this for decades, and these days Rust programmers are generally onboard with this too.
Sum types are the categorical dual of product types, so it always seems unnatural to me when a language only has the latter and not the former. I think part of the problem is we don't teach category theory to programmers, so they never stop to consider the duals of things.
I'd say the problem is many programmers don't learn enough languages to know what they're missing, or if they do they only get as far as languages with the same semantics but slightly different syntax.
> In contrast, a product type says "I have both a result and an error", which makes no sense
As an aside, there are many situations where this does make sense. Consider even the lowly Write, which returns (int, error). On a partial write, you will get the number of bytes written successfully (the result) and the error encountered.
I agree that most result/errors are sum types, but part of the philosophy of Go the programmer is ultimately in charge, and that while types are important, they can’t substitute for an actual understanding of the semantics of a given function call, etc. For example, even if Write was a sum type, what would a result of zero mean?
So while there’s value in reducing the invalid states that representable, there’s also diminishing returns. So the question is where to draw the line in the language. I agree that having sum types would be convenient, but I’m not sure that Go has made the wrong choice here, given that it seems to have found a really productive balance.
No, but we should strive to improve. If there is a clearly better solution that can be implemented reasonably, it should be. In this case, up-til now, there hasn't been.
The color choice is bad, that makes it difficult for my eyes to read. In case you hadn't realized each line is a link to the actual proposal. That's where the detail is found. The error line in the page tries to summarize the proposals way of handling errors. Common techniques are put in groups. I though it was pretty concise.
One of my favorite episodes of the 99% Invisible podcast was about “Good Egress”[0]. It talked about the history of fire escapes and why you don’t see metal fire escapes hanging out the side of buildings any longer. The reason for this is that in an emergency, people tend to exit the way they entered. Consequently, that’s why buildings now have central fire stairs.
Error handling in Go is similar. Errors are not something that live outside the flow of control. When they’re out of sight, they become like the fire escape with a barbecue on it or that’s covered in snow in the winter. Even if they’re just being passed up the call stack, they’re at least being handled and you can see exactly how. If they’re not being handled, static analysis can tell you.
It’s not perfect. I’ve had times where I’ve grown tired of writing `if err != nil`. The difference is at least I do it more often in Go, rather than scratching my head trying to figure out bespoke error management in projects written in other languages or whether my error is worthy of being an Exception.
I don’t need to punitively reread “if there is an error, stop and return it to the caller” a thousand times a day, for the same reason that I don’t need to read the code that sets up a stack frame for a function call. I think I want an editor mode that just displays the 30% that does something.
Whatever is settled upon hopefully allows for chaining and type-safety. It will be interesting to compare the results of Go language changes (= not Java) and the evolution of C# (= better Java).
I am making this comment in this good faith striving not to be dismissive. The alternative proposals here look a lot like when a freshman colleague of mine decided that Go error handling is broken and wrote a custom mechanism using `panic` and `recover` that was near impossible to read and maintain.
Truth is that the canonical way of handling errors in Go, despite looking unsightly to people from other language ecosystems and seeming cumbersome to use, actually works well in practice.
The fact that when used along with a linter that enforces error handling, I never have to worry if a certain function I'm using can `throw` and not having to wonder whether that has been handled in a try-catch provides a lot of cognitive relief.
I say this as someone who loves other languages like Rust as well. The fact that there is a canonical mechanism that everyone uses which you don't need to second guess, or jerry rig your own is a great net benefit. In Rust, it's the union Result type, in Go it's every function returning error. I would not advocate for one trying to shoehorn the other's mechanism into its own ecosystem.
I don't think I like any of these proposals. None of them add something that makes me think, "hey, I really need this". Heck, I don't think any of them would even be nice to have.
Go error handling in practice actually works very well. I've been writing Go for a long time and I've never had an issue with it. It's not as fancy as Rust but it works and works well, so if it ain't broke, please don't try fixing it.
I've heard the same thing about C and why is C++ or Java necessary. Ultimately, I believe programming languages should strive to prevent mistakes early on in the lifecycle of a program. One easy way to do that in production software is to unify the control flow of happy-paths and error-paths (something go already does) and make sure that the error-path is acknowledged and not ignored (something it does not do).
The weakness in the current design is three-fold for me:
- I can "forget" to handle in error (ok this can be linted for maybe?)
- There is a lot of ceremony in propagating errors up the chain (a very very very common thing to do!)
- The convention of (ok, err) is not always conformed to and ultimately provides no benefit over more concrete typings like Result<T, Err>. Now I can easily grep for fallible operations or operations that call fallible operations!
You may be fine with it - I hear the same argument from my coworkers about checked exceptions in java. I think we can do better.
> Go error handling in practice actually works very well.
I disagree. A quick look through the Go code I'm working with now shows more than half the functions that check for and return errors have absolutely no business knowing about the error. These places are in the middle of the call chain, well away from the place that generated or first encountered the error. These functions check the "if err != nil" and bubble them up. That's just noise. Go's error handling in these cases color everything in their path.
28 comments
[ 4.7 ms ] story [ 85.5 ms ] threadtype error interface { Error() string }
and then
if err != nil { return err } 10000x times
counts as a legitimate error handling strategy, you either have an extremely low bar set or haven't used Go much. Then again, people seem to be okay with Golang allowing nil pointer exceptions to exist in 2021, so maybe the bar is underground.
And when you're constructing your return values, often if you have an error, you won't have a real `foo` to give back, which raises the temptation to return a pointer-to-foo instead so you can give nil in that case.
But then your caller has to check for the nonsense case where there's no error but also a nil foo.
Usually what is meant is "I'll return a foo _or_ an error" but what go's type system encodes is "I'll give you a foo and maybe an error" or "I'll give you maybe a foo and maybe an error" depending on what choice you make.
When using static types, it is best to make as few nonsensical/illegal/invalid states representable as possible, so that you can rely on the compiler enforcing that they won't happen. Functional programmers have been saying this for decades, and these days Rust programmers are generally onboard with this too.
Sum types are the categorical dual of product types, so it always seems unnatural to me when a language only has the latter and not the former. I think part of the problem is we don't teach category theory to programmers, so they never stop to consider the duals of things.
As an aside, there are many situations where this does make sense. Consider even the lowly Write, which returns (int, error). On a partial write, you will get the number of bytes written successfully (the result) and the error encountered.
I agree that most result/errors are sum types, but part of the philosophy of Go the programmer is ultimately in charge, and that while types are important, they can’t substitute for an actual understanding of the semantics of a given function call, etc. For example, even if Write was a sum type, what would a result of zero mean?
So while there’s value in reducing the invalid states that representable, there’s also diminishing returns. So the question is where to draw the line in the language. I agree that having sum types would be convenient, but I’m not sure that Go has made the wrong choice here, given that it seems to have found a really productive balance.
Error handling in Go is similar. Errors are not something that live outside the flow of control. When they’re out of sight, they become like the fire escape with a barbecue on it or that’s covered in snow in the winter. Even if they’re just being passed up the call stack, they’re at least being handled and you can see exactly how. If they’re not being handled, static analysis can tell you.
It’s not perfect. I’ve had times where I’ve grown tired of writing `if err != nil`. The difference is at least I do it more often in Go, rather than scratching my head trying to figure out bespoke error management in projects written in other languages or whether my error is worthy of being an Exception.
EDIT: added link to 99PI
[0] https://99percentinvisible.org/episode/good-egress/
Truth is that the canonical way of handling errors in Go, despite looking unsightly to people from other language ecosystems and seeming cumbersome to use, actually works well in practice.
The fact that when used along with a linter that enforces error handling, I never have to worry if a certain function I'm using can `throw` and not having to wonder whether that has been handled in a try-catch provides a lot of cognitive relief.
I say this as someone who loves other languages like Rust as well. The fact that there is a canonical mechanism that everyone uses which you don't need to second guess, or jerry rig your own is a great net benefit. In Rust, it's the union Result type, in Go it's every function returning error. I would not advocate for one trying to shoehorn the other's mechanism into its own ecosystem.
Go error handling in practice actually works very well. I've been writing Go for a long time and I've never had an issue with it. It's not as fancy as Rust but it works and works well, so if it ain't broke, please don't try fixing it.
The weakness in the current design is three-fold for me: - I can "forget" to handle in error (ok this can be linted for maybe?) - There is a lot of ceremony in propagating errors up the chain (a very very very common thing to do!) - The convention of (ok, err) is not always conformed to and ultimately provides no benefit over more concrete typings like Result<T, Err>. Now I can easily grep for fallible operations or operations that call fallible operations!
You may be fine with it - I hear the same argument from my coworkers about checked exceptions in java. I think we can do better.
I disagree. A quick look through the Go code I'm working with now shows more than half the functions that check for and return errors have absolutely no business knowing about the error. These places are in the middle of the call chain, well away from the place that generated or first encountered the error. These functions check the "if err != nil" and bubble them up. That's just noise. Go's error handling in these cases color everything in their path.