This is a proposal to add a Result Type to Go. Result types typically contain either a returned value or an error, and could provide first-class encapsulation of the common (value, err)
pattern ubiquitous throughout Go programs.
My apologies if something like this has been submitted before, but hopefully this is a fairly comprehensive writeup of the idea.
Some background on this idea can be found in the post Error Handling in Go, although where that post suggests the implementation leverage generics, I will propose that it doesn't have to, and that in fact result types could (with some care) be retrofitted to Go both without adding generics and without making any breaking changes to the language itself.
That said, I am self-applying the "Go 2" label not because this is a breaking change, but because I expect it will be controversial and, to some degree, going against the grain of the language.
The Rust Result type provides some precedent. A similar idea can be found in many functional languages, including Haskell's Either, OCaml's result, and Scala's Either. Rust manages errors quite similarly to Go: errors are just values, bubbling them up is handled at each call site as opposed to the spooky-action-at-a-distance of exceptions using non-local jumps, and some work may be needed to convert error types or wrap errors into error-chains.
Where Rust uses sum types (see Go 2 sum types proposal) and generics to implement result types, as a special case core language feature I think a Go result type doesn't need either, and can simply leverage special case compiler magic. This would involve special syntax and special AST nodes much like Go's collection types presently use.
I believe the addition of a Result Type to Go could have the following positive outcomes:
if err != nil { return nil, err }
"pattern" (or minor variations thereof) can be seen everywhere in Go programs. This boilerplate adds no value and only serves to make programs much longer.First a quick note: please don't let the idea get too mired in syntax. Syntax is a very easy thing to bikeshed, and I don't think any of these examples serve as the One True Syntax, which is why I'm giving several alternatives.
Instead I'd prefer people pay attention to the general "shape" of the problem, and only look at these examples to better understand the idea.
Simplest thing that works: just add "result" in front of the return value tuple:
func f1(arg int) result(int, error) {
More typical is a "generic" syntax, but this should probably be reserved for if/when Go actually adds generics (a result type feature could be adapted to leverage them if that ever happened):
func f1(arg int) result<int, error> {
When returning results, we'll need a syntax to wrap values or errors in a result type. This could just be a method invocation:
return result.Ok(value)
```go
return result.Err(error)
If we allow "result" to be shadowed here, it should avoid breaking any code that already uses "result".
Perhaps "Go 2" could add syntax sugar similar to Rust (although it would be a breaking change, I think?):
```go
return Ok(value)
```go
return Err(value)
### Propagating errors
Rust recently added a `?` operator for propagating errors (see [Rust RFC 243](https://github.com/rust-lang/rfcs/blob/master/text/0243-trait-based-exception-handling.md)). A similar syntax could enable replacing `if err != nil { return _, err }` boilerplate with a shorthand syntax that bubbles the error up the stack.
Here are some prospective examples. I have only done some cursory checking for syntactic ambiguity. Apologies if these are either ambiguous or breaking changes: I assume with a little work you can find a syntax for this which isn't at breaking change.
First, an example with present-day Go syntax:
```go
count, err = fd.Write(bytes)
if err != nil {
return nil, err
}
Now with a new syntax that consumes a result and bubbles the error up the stack for you. Please keep in mind these examples are only for illustrative purposes:
count := fd.Write!(bytes)
```go
count := fd.Write(bytes)!
```go
count := fd.Write?(bytes)
```go
count := fd.Write(bytes)?
```go
count := try(fd.Write(bytes))
NOTE: Rust previously supported the latter, but has generally moved away from it as it isn't chainable.
In all of my subsequent examples, I'll be using this syntax, but please note it's just an example, may be ambiguous or have other issues, and I'm certainly not married to it:
count := fd.Write(bytes)!
The syntax proposals all use a result
keyword for identifying the type. I believe (but am certainly not certain) that shadowing rules could be developed that would allow existing code using "result" for e.g. a variable name to continue to function as-is without issue.
Ideally it should be possible to "upgrade" existing code to use result types in a completely seamless manner. To do this, we can allow results to be consumed as a 2-tuple, i.e. given:
func f1(arg int) result(int, error) {
It should be possible to consume it either as:
result := f1(42)
or:
(value, err) := f1(42)
That is to say, if the compiler sees an assignment from result(T, E)
to (T, E)
, it should automatically coerce. This should allow functions to seamlessly switch to using result types.
Commonly error handling will be a lot more involved than if err != nil { return _, err }
. This proposal would be woefully incomplete if that were the only case it helped with.
Result types are known for being something of a swiss knife of error handling in functional languages due to the "combinators" they support. Really these combinators are just a set of methods which allow us to transform and selectively behave based on a result type, typically in "combination" with a closure.
Then()
: chain together function calls that return the same result typeLet's say we had some code that looks like this:
resp, err := doThing(a)
if err != nil {
return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
return nil, err
}
With a result type, we can create a function that takes a closure as a parameter and only calls the closure if the result was successful, otherwise short circuiting and returning itself it it represents an error. We'll call this function Then
(it's described this way in the Error Handling in Go) blog post, and known as and_then
in Rust). With a function like this, we can rewrite the above example as something like:
result := doThing(a).
Then(func(resp) { doAnotherThing(b, resp.foo()) }).
Then(func(resp) { FinishUp(c, resp.bar()) })
if result.isError() {
return result.Error()
}
or using one of the proposed syntaxes from above (I'll pick !
as the magic operator):
final_value := doThing(a).
Then(func(resp) { doAnotherThing(b, resp.foo()) }).
Then(func(resp) { FinishUp(c, resp.bar()) })!
This reduces the 12 lines of code in our original example down to three, and leaves us with the final value we're actually after and the result type itself gone from the picture. We never even had to give the result type a name in this case.
Now granted, the closure syntax in that case feels a little unwieldy/JavaScript-ish. It could probably benefit from a more lightweight closure syntax. I'd personally love something like this:
final_value := doThing(a).
Then(|resp| doAnotherThing(b, resp.foo())).
Then(|resp| FinishUp(c, resp.bar()))!
...but something like that probably deserves a separate proposal.
Map()
and MapErr()
: convert between success and error valuesOften times when doing the if err != nil { return nil, err }
dance you'll want to actually do some handling of the error or transform it to a different type. Something like this:
resp, err := doThing(a)
if err != nil {
return nil, myerror.Wrap(err)
}
In this case, we can accomplish the same thing using MapErr()
(I'll again use !
syntax to return the error):
resp := doThing(a).
MapErr(func(err) { myerror.Wrap(err) })!
Map
does the same thing, just transforming the success value instead of the error.
There are many more combinators than the ones I have shown here, but I believe these are the most interesting. For a better idea of what a fully-featured result type looks like, I'd suggest checking out Rust's:
Language change proposals are not currently being considered during the proposal review process, as the Go 1.x language is frozen (and this is Go2, as you've noted). Just letting you know to not expect a decision on this anytime soon.
final_value := doThing(a).
Then(func(resp) { doAnotherThing(b, resp.foo()) }).
Then(func(resp) { FinishUp(c, resp.bar()) })!
I think this wouldn't be the right direction for Go. ()) })!
, seriously? The main goal of Go should be ease of learning, readability and ease of use. This doesn't help.
As someone said in the reddit thread: would definitely prefer proper sum types and generics rather than new special builtins.
Perhaps I was unclear in the post: I would certainly prefer a result type be composed from sum types and generics.
I was attempting to spec this in such a way that the addition of both (which I personally consider to be extremely unlikely) wouldn't be a blocker for adding this feature, and it could be added in such a way that, when available, this feature could switch to them (I even gave an example of what it would look like with a traditional generic syntax, and also linked to the Go sum type issue).
I don't understand the connection between the result type and the goals. Your ideas about error propagation and combinators appear to work just as well with the current support for multiple result parameters.
@ianlancetaylor can you give an example of how to define a combinator that works generically on the current result tuples? If it's possible I'd be curious to see it, but I don't think it is (per this post)
@tarcieri That post is significantly different, in that error
does not appear in its suggested use of Result<A>
. This issue, unlike the post, seems to be suggesting result<int, error>
, which to me implies that the proposed combinators are specially recognizing error
. My apologies if I misunderstand.
The intent is not to couple result
to error
, but for result
to carry two values, similar to the Result
type in Rust or Either
in Haskell. In both languages, by convention the second value is usually an error
type (although it doesn't have to be).
This issue, unlike the post, seems to be suggesting result
The post suggests:
type Result<A> struct {
// fields
}
func (r Result<A>) Value() A {…}
func (r Result<A>) Error() error {…}
...so, to the contrary, that post specializes around error
, whereas this proposal accepts a user-specified type for the second value.
Admittedly things like result.Err()
and result.MapErr()
give a nod to this value always being an error
.
@tarcieri What's wrong with a struct? https://play.golang.org/p/mTqtaMbgIF
@griesemer as is covered in the Error Handling in Go post, that struct is not generic. You would have to define one for every single combination of success and error types you ever wanted to use.
@tarcieri Understood. But if that (non-genericity, or perhaps not having a sum type) is the problem here, than we should address those issues instead. Handling result types only is just adding more special cases.
Whether or not Go has generics is orthogonal to whether a first-class result type is useful. It would make the implementation closer to something you implement yourself, but as covered in the proposal allowing the compiler to reason about it in a first class manner allows it e.g. to warn for unconsumed results. Having a single result type is also what makes the combinators in the proposal composable.
@tarcieri Composition as you suggested would also be possible with a single result struct type.
I don't understand why you wouldn't use an embedded or defined struct type. Why have specialized methods and syntax for checking errors? Go already has means of doing all of this. It seems like this is just adding features that don't define the Go language, they define Rust. It would be a mistake to implement such changes.
I don't understand why you wouldn't use an embedded or defined struct type. Why have specialized methods and syntax for checking errors?
To repeat myself again: Because having a generic result type requires... generics. Go does not have generics. Short of Go getting generics, it needs special-case support from the language.
Perhaps you're suggesting something like this?
type Result struct {
value interface{}
err error
}
Yes, this "works"... at the cost of type safety. Now to consume any result we have to do a type assertion to make sure the interface{}
-typed value is the one we're expecting. If not, it's now become a runtime error (as opposed to a compile time error as it is presently).
That would be a major regression over what Go has now.
For this feature to actually be useful, it needs to be type safe. Go's type system is not expressive enough to implement it in a type-safe manner without special-case language support. It would need generics at a minimum, and ideally sum types as well.
It seems like this is just adding features that don't define the Go language [...]. It would be a mistake to implement such changes.
I covered as much in the original proposal:
"I'll admit this approach comes with a bit of a learning curve, and as such can negatively impact the clarity of programs for people who are unfamiliar with combinator idioms. Though personally I love combinators for error handling, I can definitely see how culturally they may be a bad fit for Go."
I feel like I have confirmed my suspicions and that a feature like this both isn't easily understood by Go developers and goes against the simplicity-oriented nature of the language. It's leveraging programming paradigms that, quite clearly, Go developers don't seem to understand or want, and in such case seems like a misfeature.
they define Rust
Result types aren't a Rust-specific feature. They're found in many functional languages (e.g. Haskell's Either
and OCaml's result
). That said, introducing them into Go feels like a bridge too far.
Thank you for sharing your ideas, but I think the examples used above are unconvincing. To me, A is better than B:
A
```resp, err := doThing(a)
if err != nil {
return nil, err
}
if resp, err = doAnotherThing(b, resp.foo()); err != nil {
return err
}
if resp, err = FinishUp(c, resp.bar()); err != nil {
return err
}
result := doThing(a).
Then(func(resp) { doAnotherThing(b, resp.foo()) }).
Then(func(resp) { FinishUp(c, resp.bar()) })
if result.isError() {
return result.Error()
}
```
I don't think A is more readable. In fact, the actions aren't noticeable at all. Instead, the first glance reveals that a bunch of errors are being obtained and returned.
If B were to be formatted so that the closure bodies were on new lines, that would've been the most readable format.
Also, the last point seems a bit silly. If function call performance is so important, then by all means, go with a more traditional syntax.
A
From @as I think we normal flow should no indent.
if err != nil {
return err
}
resp, err = doAnotherThing(b, resp.foo());
if err != nil {
return err
}
resp, err = FinishUp(c, resp.bar());
if err != nil {
return err
}
One interesting observation from this thread: the original example I gave which people keep copying and pasting contained some errors (the first if
returned nil, err
on error, the subsequent two only return err
). These errors were not deliberate on my part, but I think it's an interesting case study.
Though this particular class of error is the sort that would've been caught by the Go compiler, I think it's interesting to note that how with so much syntactic boilerplate, it becomes very easy to look past such errors when copying and pasting.
This doesn't make the proposal better. It's an assumption that failing to return multiple values is a result of explicit error handling. You could have also made the same errors inside the functions, you just wouldn't have seen them due to their unnecessary encapsulation.
I disagree, I think that is a strong point of this kind of proposal. If all a program is doing is returning the err and not processing it, then it is wasting cognitive overhead and code and making things less readable. Adding a feature like this would mean that (in projects that to choose to use it) code that deals with errors is actually doing something worth understanding.
We will have to agree to disagree. The magic tokens in the proposal are easy to write, but difficult to understand. Just because we have made it shorter doesn't mean we've made it simpler.
Making things less readable is subjective, so here's my opinion. All I see in this proposal is more complex and obscure code with magic functions and symbols (which are very easy to miss). And all they do is hide a very simple and easy to understand code in case A. For me, they don't add any value, don't shorten the code where it matters or simplify things. I don't see any value in treating them at a language level.
The only problem that the proposal solves, that I could see clearly, is boilerplate in error handling. If that's the only reason, then it's not worth it to me. The argument about syntactic boilerplate is actually working against proposal. It's much more complex in that regard - all those magic symbols and brackets that are so easy to miss. Example A has boilerplate but it doesn't cause logic errors. In that context, there's nothing to gain from that proposal, again, making it not very useful.
Let's leave Rust features to Rust.
To clarify, I'm not wild about adding the !
suffix as a shortcut, but I do like the idea of coming up with a simple syntax that simplifies
err = foo()
if err != nil {
return err
}
Even if that syntax is a keyword instead of a special symbol. It's my biggest complaint about the language (even bigger than Generics personally), and I think the littering of that pattern across the code makes it harder to read and noisy.
I also would love to see something that enables the kind of chaining @tarcieri brings up, as I find it more readable in code. I think the complexity @creker alludes to is balanced by the better signal-to-noise ratio in the code.
I don't fully understand how this proposal would achieve its stated goals.
Reduce error handling boilerplate: the proposal has some hypothetical Go code:
result := doThing(a).
Then(func(resp) { doAnotherThing(b, resp.foo()) }).
Then(func(resp) { FinishUp(c, resp.bar()) })
if result.isError() {
return result.Error()
}
I'm not really sure how func(resp) { expr }
is supposed to work without more extensive changes to the way function literals work. I think the resulting code would end up looking more like this:
result := doThing(a).
Then(func(resp T) result(T, error) { return doAnotherThing(b, resp.foo()) }).
Then(func(resp T) result(T, error) { return FinishUp(c, resp.bar()) })
if result.isError() {
return result.Error()
}
In realistic Go code, it is also quite common for the intermediate expressions to be longer than this and to need to be put on their own lines. This happens naturally in real Go code today; under this proposal, it would be:
result := doThing(a).
Then(func(resp T) result(T, error) {
return doAnotherThing(b, resp.foo())
}).
Then(func(resp T) result(T, error) {
return FinishUp(c, resp.bar())
})
if result.isError() {
return result.Error()
}
Either way, this strikes me as okay, but not great, just like the real Go code above it in the proposal. Its 'Then' combinator is essentially the opposite of 'return'. (If you are familiar with monads, this will not come as a surprise.) It removes the requirement to write an 'if' statement, but introduces the requirement to write a function. Overall, it's not substantially better or worse; it is the same boilerplate logic with a new spelling.
Allows the compiler to reason about results: if this feature is desirable (and I'm not expressing any opinion about that here), I don't see how this proposal makes it substantially more or less feasible. They strike me as orthogonal.
In most well-written Go, this kind of error-handling boilerplate makes up a small fraction of code. It was a single-digit percentage of lines in my brief look at some Go codebases I consider to be well-written. Yes, it is sometimes appropriate, but often it's a sign that some redesign is in order. In particular, simply returning an error without adding any context whatsoever happens more often than it should today. It might be called an "anti-idiom". There's a discussion to be had around what, if anything, Go should or could do to discourage this anti-idiom, either in the language design, or in the libraries, or in the tooling, or purely socially, or in some combination of those. I would be equally interested to have that discussion whether or not this proposal is adopted. In fact, making that anti-idiom easier to express, as I believe is the aim of this proposal, might set up the wrong incentives.
At the moment, this proposal is being treated largely as matter of taste. What would make it more compelling in my opinion would be evidence demonstrating that its adoption would reduce the total amount of bugs. A good first step might be converting a representative chunk of the Go corpus to demonstrate that some sorts of bugs are impossible or unlikely to be expressed in the new style — that x bugs per line in actual Go code in the wild would be fixed by using the new style. (It seems much harder to demonstrate that the new style doesn't offset any improvement by making other sorts of bugs more likely. There we might have to make do with abstract arguments about readability and complexity, like in the bad old days before the Go corpus rose to prominence.)
With supporting evidence like that in hand, one could make a stronger case.
Simply returning an error without adding any context whatsoever happens more often than it should today. It might be called an "anti-idiom".
I'd like to echo this sentiment. This
if err := foo(x); err != nil {
return err
}
should not be simplified, it should be discouraged, in favor of e.g.
if err := foo(x); err != nil {
return errors.Wrapf(err, "fooing %s", x)
}
@peterbourgon
my biggest problem with this is not that the error is returned blindly. It's the fact that the action: foo(x)
; isn't that visible, and imho makes the whole thing quite a bit less readable than alternate 'functional' solutions, where the action itself is a simple return on a new line.
even if the assignment and action is kept separate from the if statement itself, the resulting statement would still put an accent on the result, rather than the action. That is perfectly valid, especially if the result is the important part. But if you have a bunch of statements, where each one gets a (result, error) tuple, checks the error/returns, then proceeds to do another action while obtaining a new tuple, the results themselves are obviously not the main characters in the plot.
@urandom I think result is pair of (val, error) so I think checks the error/returns are the main characters in the plot too.
What about a reserved word (something like reterr
) to avoid all the if err != nil { return err }
?
So this
resp, err := doThing(a)
if err != nil {
return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
return nil, err
}
Would become:
resp, _ := reterr doThing(a)
resp, _ = reterr doAnotherThing(b, resp.foo())
resp, _ = reterr FinishUp(c, resp.bar())
reterr would basically check the return values of the called function and return if any of them is error and is not nil (and return nil in any non-error return value).
Sounds more and more as #18721
@tarcieri Just use some of reflect
package. I can simulate something like your proposal.
But I think it not worth to do it.
https://play.golang.org/p/CC5txvAc0e
func main() {
result := Do(func() (int, error) {
return doThing(1000)
}).Then(func(resp int) (int, error) {
return doAnotherThing(200000, resp)
}).Then(func(resp int) (int, error) {
return finishUp(1000000, resp)
})
if result.err != nil {
log.Fatal(result.err)
}
val := result.val.(int)
fmt.Println(val)
}
@iporsut there are two problems with reflection which make it an unsuitable solution to this particular problem, although it may appear to "solve" the problem on the surface:
To me either of these problems are a huge step backward from what Go has already, and in tandem they're a complete nonstarter.
I like Go and the way it handles errors. However, maybe it could be simpler. Here are some of my ideas regarding error handling in Go.
The way it is now:
resp, err := doThing(a)
if err != nil {
return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
return nil, err
}
A:
resp, _ := doThing(a)
resp, _ = doAnotherThing(b, resp.foo())
resp, _ = FinishUp(c, resp.bar())
// return if error is omited, otherwise deal with it as usual (if err != nil { return err })
//However, this breaks semantics of Go and may mislead due to the usa of _ (__ or !_ could be used to avoid such misleading)
B:
resp, err := doThing(a)?
resp, err = doAnotherThing(b, resp.foo())?
resp, err = FinishUp(c, resp.bar())?
// ? indicates that it will return in case of error (more explicit)
// or any other indication could be used
// this approach is preferred for its explicitness
C:
resp, err := doThing(a)
return if err
resp, err = doAnotherThing(b, resp.foo())
return if err
resp, err = FinishUp(c, resp.bar())
return if err
// if err return err
// or if err return (similar to javascript return)
// this one is my favorite, almost no changes to the language, very readable and less SLOC
D:
resp, _ := return doThing(a)
resp, _ = return doAnotherThing(b, resp.foo())
resp, _ = return FinishUp(c, resp.bar())
// or
resp = throw FinishUp(c, resp.bar())
// this one is also very readable (although maybe a litle less than option **C**) and even less SLOC than **C**
// at this point I'm not sure whether C or D is my favorite ))
//This applies to all approaches above
// if the function that contains any of these options has no value to return, exit the function. E.g.:
func test() {
resp, _ := return doThing(a) // or any of other approaches
// exit function
}
func test() ([]byte, error) {
resp, _ := return doThing(a) // or any of other approaches
// return whatever is returned by doThing(a) (this function of course must return ([]byte, error))
}
Excuse my English and I am not sure whether such changes are possible and whether they will result in performance overhead.
If you like any of these approaches, please like them following next rules:
A = 👍
B = 😄
C = ❤️
D = 🎉
And 👎 if you dislike the whole idea ))
This way we can have some statistics and avoid unnecessary comments like "+1"
Eloborating on my "proposals"...
// no need to explicitely define error in return statement, much like throw, try {} catch in java
func test() int {
resp := throw doThing() // "returns" error if doThing returns (throws) an error
return resp // yep, resp is int
}
func main() {
resp, err := test() // the last variable is always error type
if err != nil {
os.Exit(0)
}
}
Again, not sure if something like that is possible at all ))
Here's another crazy option, make the word error
a little more magic. It becomes usable on the left-hand side of an assignment (or short declaration) and works sort of like a magic function:
res, error() := doThing()
// Shorthand for
res, err := doThing()
if err != nil {
return 0, ..., 0, err
}
Specifically, the behaviour of error()
is as follows:
error
for the purposes of assignment.nil
is assigned to it, nothing happens.nil
value is assigned to it, the enclosing function immediately returns. All return values are set to 0 except for the the last, which must be of type error
and which is assigned the value assigned to error()
.If you want to apply some mutation to the error, then you can do:
res, error(func (e error) error { return fmt.Errorf("foo: %s", error)})
:= doThing()
In which case the closure is applied to the value assigned before the function returns.
This is a bit ugly, in large part due to the syntactic bloat of having to deal with closures. The standard library could fix this well, with e.g. error(errors.Wrapper("foo"))
which will generate the correct wrapper closure for you.
As an alternative, if the nullary error()
syntax is too likely to be missed, I'd suggest error(return)
as an alternative; use of the keyword reduces risk of misinterpretation. It doesn't extend well to the closure case, however.
Everyone who's written Go has encountered the unfortunate proliferation of error handling boilerplate that distracts from the core purpose of their code. That's why Rob Pike addressed the subject in 2015. As Martin Kühl points out, Rob's proposal for simplifying error handling:
leaves us having to implement artisanal one-off monads for every interface we want to handle errors for, which I think is still as verbose and repetitive
Which is why there's so much engagement on this topic still today.
Ideally we can find a solution which:
I propose the introduction of a new keyword catch:
which works as follows:
Instead of the current form:
res, err := doThing()
if err != nil {
return 0, ..., 0, err
}
we would write:
res, err := doThing() catch: 0, ..., 0, err
which would behave in exactly the same manner as the current form code above. More specifically, the function and assignments to the left of the catch:
are executed first. Then, if and only if exactly one of the return arguments is of type error
AND that value is non-nil, the catch:
acts as a return
statement with the values to the right. If there are zero or more than one error
type returned from doThing()
, it's a syntax error to use catch:
. If the error value returned from doThing()
is nil
, then everything from catch:
to the end of the statement is ignored and not evaluated.
To give a more complex example from Nemanja Mijailovic's recent blog post entitled, Error handling patterns in Go:
func parse(r io.Reader) (*point, error) {
var p point
if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {
return nil, err
}
return &p, nil
}
This becomes instead:
func parse(input io.Reader) (*point, error) {
var p point
err := read(&p.Longitude) catch: nil, errors.Wrap(err, "Failed to read longitude")
err = read(&p.Latitude) catch: nil, errors.Wrap(err, "Failed to read Latitude")
err = read(&p.Distance) catch: nil, errors.Wrap(err, "Failed to read Distance")
err = read(&p.ElevationGain) catch: nil, errors.Wrap(err, "Failed to read ElevationGain")
err = read(&p.ElevationLoss) catch: nil, errors.Wrap(err, "Failed to read ElevationLoss")
return &p, nil
}
Advantages:
Disadvantages:
catch:
either be a new error or a wrapped error, but not identical to the error returned by the function to the left of the catch:
. Go has been described as "opinionated" and such strictness on error handling for the sake of clarity and reliability would have fit with that. I lacked the creativity to incorporate that goal, though.catch:
without an explicit throw? The reserved word doesn't necessarily have to be catch:
. Others may have better ideas. It could even be an operator instead of a reserved word.Everyone who's written Go has encountered the unfortunate proliferation of error handling boilerplate that distracts from the core purpose of their code.
That is not true. I do program in Go quite a lot and I do not have any problem with any error handling boilerplate. Writing error handling code consumes such a microscopic fraction of time developing a project that I hardly notice it and it IMHO it does not justify any change to the language.
Everyone who's written Go has encountered the unfortunate proliferation of error handling boilerplate that distracts from the core purpose of their code.
That is not true. I do program in Go quite a lot and I do not have any problem with any error handling boilerplate. Writing error handling code consumes such a microscopic fraction of time developing a project that I hardly notice it and it IMHO it does not justify any change to the language.
I didn't say anything about how much time writing error handling code takes. I only said that it distracts from the core purpose of the code. Maybe I should have said "Everyone who's read Go has encountered the unfortunate proliferation of error handling...".
So, @cznic, I guess the question for you is whether you've read Go code that you felt had an excessive amount of error handling boilerplate or which distracted from the code you were trying to understand?
No one likes my proposals 😅
Anyways, we should have some syntax, and vote for best one (some poll system) and include link here or in readme
Maybe I should have said "Everyone who's read Go has encountered the unfortunate proliferation of error handling...".
That's not true. I prefer the explicitness and proper locality of the current state of art of error handling. The proposal, as any other I have ever seen, makes the code IMHO less readable and worse to maintain.
So, @cznic, I guess the question for you is whether you've read Go code that you felt had an excessive amount of error handling boilerplate or which distracted from the code you were trying to understand?
No. Go is in my experience an exceptionally well readable programming language. Half of that credit goes to gofmt, of course.
My own experience is that it really starts to drag when you have a bunch of dependent statements, each of which can throw an error, the error handling adds up and gets old fast. What could be 5 lines of code becomes 20.
@cznic
In my experience, having so much error handling boilerplate makes the code much less readable. Because the error handling itself is mostly identical (sans any error wrapping that might occur), it produces a sort-of fence effect, where if you quickly scan through a piece of code, you mostly end up seeing a mass of error handling. Thus the biggest problem, the actual code, the most important part of the program, is hidden behind this optical illusion, making it that much difficult to actually see what a piece of code is about.
Error handling shouldn't the main part of any code. Unfortunately, quite often it ends up being exactly that.
There's a reason statement composition in other languages is so popular.
Because the error handling itself is mostly identical (sans any error
wrapping that might occur), it produces a sort-of fence effect, where if
you quickly scan through a piece of code, you mostly end up seeing a mass
of error handling.
This is a highly subjective position. It's like arguing that if statements
make the code unreadable, or that K&R style braces make things unreadable.
From my point of view the explicitness of go's error handling quickly fades
into the background of familiarity until you notice the pattern broken;
something the human eye is very good at doing; missing error handling,
error variables assign to _, etc.
It _is_ a burden to type, make no mistake. But Go does not optimise for the
code author, it explicitly optimises for the reader.
On Tue, May 16, 2017 at 5:45 PM, Viktor Kojouharov <[email protected]
wrote:
@cznic https://github.com/cznic
In my experience, having so much error handling boilerplate makes the code
much less readable. Because the error handling itself is mostly identical
(sans any error wrapping that might occur), it produces a sort-of fence
effect, where if you quickly scan through a piece of code, you mostly end
up seeing a mass of error handling. Thus the biggest problem, the actual
code, the most important part of the program, is hidden behind this optical
illusion, making it that much difficult to actually see what a piece of
code is about.Error handling shouldn't the main part of any code. Unfortunately, quite
often it ends up being exactly that.—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/19991#issuecomment-301702623, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAAcA4ydpBFiapYBOBUyUjg6du5Dnjs5ks5r6VQjgaJpZM4M-dud
.
if you quickly scan through a piece of code, you mostly end up seeing a mass
of error handling.
This is a highly subjective position.
Highly subjective yet widely shared.
As Rob himself said,
A common point of discussion among Go programmers, especially those new to the language, is how to handle errors. The conversation often turns into a lament at the number of times the sequence
if err != nil {
return err
}
shows up.
In fairness, Rob went on to say this perception about Go error handling is "unfortunate, misleading, and easily corrected." However, he spends most of that article explaining his recommended method for correcting the perception. Unfortunately, Rob's prescription is problematic in itself as explained so well by Martin Kühl. In addition to Martin's critique, Rob's suggestion also reduces the locality which @cznic says he values in Go error handling.
Maybe the question is if we had the ability to replace
res, err := doThing()
if err != nil {
return nil, err
}
with something similar to:
res, err := doThing() catch: nil, err
Would you use it, or would you stick with the four line version? Regardless of your personal preference, do you think an alternative like this would be widely adopted by the Go community and become idiomatic? Given the subjectivity of any argument that the shorter version adversely affects readability, my experience with programmers says they would strongly gravitate toward the single line version.
Real talk : go 1 is fixed, and will not change, especially in this fundamental way.
It's pointless to propose some kind of option type until Go 2 implements some for of templates type. At that point, everything changes.
On 16 May 2017, at 23:46, Billy Hinners notifications@github.com wrote:
if you quickly scan through a piece of code, you mostly end up seeing a mass
of error handling.This is a highly subjective position.
Highly subjective yet widely shared.
As Rob himself said,
A common point of discussion among Go programmers, especially those new to the language, is how to handle errors. The conversation often turns into a lament at the number of times the sequence
if err != nil {
return err
}
shows up.In fairness, Rob went on to say this perception about Go error handling is "unfortunate, misleading, and easily corrected." However, he spends most of that article explaining his recommended method for correcting the perception. Unfortunately, Rob's prescription is problematic in itself as explained so well by Martin Kühl. In addition to Martin's critique, Rob's suggestion also reduces the locality which @cznic says he values in Go error handling.
Maybe the question is if we had the ability to replace
res, err := doThing()
if err != nil {
return nil, err
}
with something similar to:res, err := doThing() catch: nil, err
Would you use it, or would you stick with the four line version? Regardless of your personal preference, do you think an alternative like this would be widely adopted by the Go community and become idiomatic? Given the subjectivity of any argument that the shorter version adversely affects readability, my experience with programmers says they would strongly gravitate toward the single line version.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or mute the thread.
It's pointless to propose some kind of option type until Go 2 implements some for of templates type. At that point, everything changes.
I assumed we were talking about Go 2 as implied by the title of this thread and in the full belief that "Go 2" is not a euphemism for "never". In fact, given that Go 1 is fixed, we should probably be devoting a much larger portion of our Go discussions to Go 2.
With that said I think everyone who complains about the verbosity of Go's
error handling is missing the fundamental point that the purpose of error
handling in Go is _not_ to make the not error case as brief and unobtrusive
as possible. Rather the goal of Go's error handling strategy is to force
the writer of the code to consider, at all times, what happens when the
function fails, and, most importantly, how to clean up, undo, and recover
before returning to the caller.
All the stragies for hiding error handling boiler plate seem to me to be
ignoring this.
On Tue, 16 May 2017, 23:51 Dave Cheney dave@cheney.net wrote:
Real talk : go 1 is fixed, and will not change, especially in this
fundamental way.It's pointless to propose some kind of option type until Go 2 implements
some for of templates type. At that point, everything changes.On 16 May 2017, at 23:46, Billy Hinners notifications@github.com wrote:
if you quickly scan through a piece of code, you mostly end up seeing a
mass
of error handling.This is a highly subjective position.
Highly subjective yet widely shared.
As Rob himself said,
A common point of discussion among Go programmers, especially those new to
the language, is how to handle errors. The conversation often turns into a
lament at the number of times the sequenceif err != nil {
return err
}shows up.
In fairness, Rob went on to say this perception about Go error handling is
"unfortunate, misleading, and easily corrected." However, he spends most of that
article https://blog.golang.org/errors-are-values explaining his
recommended method for correcting the perception. Unfortunately, Rob's
prescription is problematic in itself as explained
https://www.innoq.com/en/blog/golang-errors-monads/ so well by Martin
Kühl. In addition to Martin's critique, Rob's suggestion also reduces the
locality which @cznic https://github.com/cznic says he values in Go
error handling.Maybe the question is if we had the ability to replace
res, err := doThing()
if err != nil {
return nil, err
}with something similar to:
res, err := doThing() catch: nil, err
Would you use it, or would you stick with the four line version?
Regardless of your personal preference, do you think an alternative like
this would be widely adopted by the Go community and become idiomatic?
Given the subjectivity of any argument that the shorter version adversely
affects readability, my experience with programmers says they would
strongly gravitate toward the single line version.—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/19991#issuecomment-301787215, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAAcAwATgoJwL5WV-0nffLjLB9L86GYOks5r6ai3gaJpZM4M-dud
.
Rather the goal of Go's error handling strategy is to force
the writer of the code to consider, at all times, what happens when the
function fails, and, most importantly, how to clean up, undo, and recover
before returning to the caller.
Well, then Go didn't achieve that goal. By default, Go allows you to ignore returned errors and in many cases you wouldn't even know about that until something somewhere wouldn't work like it should. On the contrary, much hated in Go community exceptions (that's just an example to prove the point) force you to consider them because otherwise application will crash. That often leads us to problem with catching everything and ignoring but that's programmer's fault.
Basically, error handling in Go is opt-in. It's more about spoken convention that every error should be handled. The goal would be achieved if it would actually force you to handle errors. For example, with compile-time errors or warnings.
With that in mind, hiding boiler plate would not hurt anybody. Spoken convention would still hold and programmers would still opt-in to error handling as it is right now.
the goal of Go's error handling strategy is to force
the writer of the code to consider, at all times, what happens when the
function fails, and, most importantly, how to clean up, undo, and recover
before returning to the caller.
That's an inarguably noble goal. It's a goal, though, that must be balanced against the readability of the primary flow and intent of the code.
As a Go programmer, I can say to you that I do not find the verbosity of
Go's error handling to hurt it's readability. I don't see there is anything
to trade away, because I feel no discomfort _reading_ code written by other
Go programmers.
On Wed, May 17, 2017 at 12:10 AM, Billy Hinners notifications@github.com
wrote:
the goal of Go's error handling strategy is to force
the writer of the code to consider, at all times, what happens when the
function fails, and, most importantly, how to clean up, undo, and recover
before returning to the caller.That's an inarguably noble goal. It's a goal, though, that must be
balanced against the readability of the primary flow and intent of the code.—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/19991#issuecomment-301794653, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAAcAzfcu5hq86xxVj85qfOquVawHh44ks5r6a5zgaJpZM4M-dud
.
@davecheney, Whereas I agree with you that error handling should be explicit and not postponed for later (which you, of course, can do with _), there's also the strategy of "bubbling" up errors to deal with them in one function, of to add extra information or remove (before sending it to the client). My personal issue is that I have to write the same 4 lines of code over and over again
For instance:
getNewToken(id int64) (Token, error) {
user := &User{ID:id}
u, err := user.Get();
if err != nil {
return Token{}, err
}
token, err := token.New(u);
if err != nil {
return Token{}, err
}
return token, nil
}
I am not handling the error here, I'm just returning it. and when I read this kind of code, I have to skip error "handling", and difficult to find the main purpose of the code
and the code above could easily be which replaced with something like that:
getNewToken(id int64) (Token, error) {
user := &User{ID:id}
u, err := throw user.Get(); //throw should also wrap the error
token, err := throw token.New(u);
return token, nil
}
Code like that is more readable and less unnecessary (IMHO) code. And the error could be and should be handled in function where this function is used.
As a Go programmer, I can say to you that I do not find the verbosity of Go's error handling to hurt it's readability.
I agree.
On an unrelated note:
It also seems to me that a "result" type is a bit too specific of a proposal; maybe types are really just two-variant enumerated types. If there were a concept of enums, a result or option package could be created out of tree and experimented with before comitting to add it to the language and without adding lots of extra syntax or methods that can't really be reused and are only good for result types. I don't know if enums would be useful in Go or not, but if you can argue the more general case it will probably also make your case stronger for the more specific result type (I suspect; maybe I'm wrong).
func getNewToken(id int64) (Token, error) {
user := &User{ID:id}
u, err := user.Get()
if err != nil {
return Token{}, err
}
return token.New(u)
}
Seems equivalent.
On Wed, May 17, 2017 at 12:34 AM, Kiura notifications@github.com wrote:
@davecheney https://github.com/davecheney, Whereas I agree with you
that error handling should be explicit and not postponed for later (which
you, of course, can do with _), there's also the strategy of "bubbling" up
errors to deal with them in one function, of to add extra information or
remove (before sending it to the client). My personal issue is that I have
to write the same 4 lines of code over and over againFor instance:
getNewToken(id int64) (Token, error) {
user := &User{ID:id}
u, err := user.Get();
if err != nil {
return Token{}, err
}token, err := token.New(u);
if err != nil {
return Token{}, err
}
return token, nil}
I am not handling the error here, I'm just returning it. and when I read
this kind of code, I have to skip error "handling", and difficult to find
the main purpose of the codeand the code above could easily be which replaced with something like that:
getNewToken(id int64) (Token, error) {
user := &User{ID:id}
u, err := throw user.Get(); //throw should also wrap the error
token, err := throw token.New(u);
return token, nil
}
Code like that is more readable and less unnecessary (IMHO) code. And the
error could be and should be handled in function where this function is
used.—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/19991#issuecomment-301802010, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAAcA9sIRXX7RSdDcUOidpe-qLTR7unNks5r6bP3gaJpZM4M-dud
.
It also seems to me that a "result" type is a bit too specific of a proposal; maybe types are really just two-variant enumerated types. If there were a concept of enums, a result or option package could be created out of tree and experimented with before comitting to add it to the language and without adding lots of extra syntax or methods that can't really be reused and are only good for result types. I don't know if enums would be useful in Go or not, but if you can argue the more general case it will probably also make your case stronger for the more specific result type (I suspect; maybe I'm wrong).
As stated in the original proposal, a result type would ideally be implemented as a sum type (e.g. enums ala Rust's), and there is an open proposal to add them to the language.
However, sum types alone are not sufficient to implement a reusable result type library without additional language support. They also require generics.
This proposal was exploring the idea of implementing a result type which does not depend on generics, but instead relies on special case help from the compiler.
I'll just add that now having posted it, I would agree the best way to pursue this (if at all) would be with language-level generics support.
@davecheney, Indeed, in this case almost no difference, but what if you have 3-4 calls in function that return error?
P.S. I am not against the way Go1 structure of handling errors, I just think it could be better.
As stated in the original proposal, a result type would ideally be implemented as a sum type (e.g. enums ala Rust's), and there is an open proposal to add them to the language.
Sorry, I should have been more clear: I was arguing that this statement:
I think a Go result type doesn't need either, and can simply leverage special case compiler magic.
feels like a bad idea to me.
However, sum types alone are not sufficient to implement a reusable result type library without additional language support. They also require generics.
This proposal was exploring the idea of implementing a result type which does not depend on generics, but instead relies on special case help from the compiler.
I'll just add that now having posted it, I would agree the best way to pursue this (if at all) would be with language-level generics support.
Yes, fair enough; I'm agreeing with your last statement then. If we have to wait for Go 2 anyways, we might as well solve the more general problem first (assuming it actually is a problem) :)
Also, Rob Pike wrote an article about error handling as mentioned above. Whereas this approach seems to be "fixing" the problem it introduces another one: more code bloat with interfaces.
I think it's important not to confuse "explicit error handling" with "verbose error handling". Go wants to force the user to consider error handling at every step rather than delegating it away. For each function you call that may throw an error, you need to decide in some what whether or not you want to handle the error, and how. Sometimes it means you ignore the error, sometimes it means you retry, often it means you just pass it up to the caller to deal with.
Rob's article is great, and really should be a part of Effective Go 2, but it's a strategy that can only take you so far. Especially when dealing with heterogeneous callees, you have a lot of error handling to manage
I don't think it's unreasonable to consider syntactic sugar or some other facility to help with error handling. I think it's important that it doesn't undermine the fundamentals of Go error handling. For instance, establishing a function-level error handler which handles all errors that occur would be bad; it means that we're allowing the programmer to do what exception handling typically does: move the consideration of errors from a statement level issue to a block- or function-level thing. That definitely is against the philosophy.
@billyh With regards to the "Error handling patterns in Go" article, there are other solutions:
@egonelbre
These solutions are only suitable of you are doing the same type of operation repeatedly. That is not usually the case. Thus, this can hardly ever be applied in practice.
@urandom please show a realistic example then?
Sure I can take a more complicated example:
func (conversion *PageConversion) Convert() (page *kb.Page, errs []error, fatal error)
I understand that these are not applicable to everywhere, but without a proper list of examples we want to improve there's no way to have a decent discussion.
@egonelbre
Disclaimer: I haven't used juju, nor have I read the code. It's just a 'production' product I know of the top of my head. I am reasonably sure that such type of error handling (where errors are checked in between independent operations) is prevalent in the go world, and I highly doubt there's anyone out there that hasn't stumbled into this.
@urandom I agree. The main issue with discussing without real-world code is that people remember the "gist" of the problem, not the actual problem -- which often leads to over-simplified problem-statement. _PS: I remembered one nice example in go._
For example, from these real world examples we can see that there are several other things that need to be considered:
Not just the "happy" and "failure" path. I'm not saying that these cannot be solved, just that they need to be mapped out and discussed.
@egonelbre here's another example from this week's Golang Weekly, in the article by Mario Zupan entitled, "Writing a Static Blog Generator in Go":
func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
fmt.Printf("Fetching data from %s into %s...\n", from, to)
if err := createFolderIfNotExist(to); err != nil {
return nil, err
}
if err := clearFolder(to); err != nil {
return nil, err
}
if err := cloneRepo(to, from); err != nil {
return nil, err
}
dirs, err := getContentFolders(to)
if err != nil {
return nil, err
}
fmt.Print("Fetching complete.\n")
return dirs, nil
}
Note: I'm not implying any critique of Mario's code. In fact, I quite enjoyed his article.
Unfortunately, examples like this are all too common in Go source. Go code gravitates toward this train track pattern of one line of interest followed by three lines of identical or almost identical boilerplate repeated again and again. Combining the assignment and conditional where possible, as Mario does, helps a little.
I'm not sure any programming language was designed with a primary goal of minimizing lines of code, but a) the ratio of meaningful code to boilerplate could be one (of many) valid measures of the quality of a programming language, and b) because so much of programming involves error handling, this pattern pervades Go code and therefore makes this particular case of excess boilerplate merit streamlining.
If we can identify a good alternative, I believe it will be rapidly adopted and make Go even more enjoyable to read, write and maintain.
Rebecca Skinner (@cercerilla) shared an excellent writeup of Go's error handling shortcomings along with an analysis of using monads as a solution in her slide deck Monadic Error Handling in Go. I particularly liked her conclusions at the end.
Thanks to @davecheney for referring to Rebecca's deck in his article, Simplicity Debt Redux which enabled me to find it. (Thanks also to Dave for grounding my rose colored optimism for Go 2 with the grittier realities.)
Go code gravitates toward this train track pattern of one line of interest followed by three lines of identical or almost identical boilerplate repeated again and again.
Every control flow control statement is important. The error-handling lines are critically important from the correctness point of view.
the ratio of meaningful code to boilerplate could be one (of many) valid measures of the quality of a programming language
If someone considers error handling statements not meaningful then good luck with the coding and I hope to stay away from the results.
To address one of the points covered in @davecheney's Simplicity Debt Redux (which I covered, but I think it bears repeating):
The next question is, would this monadic form become the single way errors are handled?
For something like this to become the "single" way errors are handled, it would have to be a breaking change done across the entire standard library and every "Go2" compatible project. I think that's unwise: the Python2/3 debacle shows how schisms like that can be damaging to language ecosystems.
As mentioned in this proposal, if a result type could automatically coerce to the equivalent tuple form, you could have your cake and eat it too in terms of a hypothetical Go2 standard library adopting this approach across the board while still maintaining backwards compatibility with existing code. This would allow those who are interested to take advantage of it, but libraries which still wish to work on Go1 will just work out-of-the-box. Library authors could have their choice: write libraries that work on both Go1 and Go2 using the old style, or Go2-only using the monadic style.
The "old way" and the "new way" of error handling could be compatible to the point users of the language wouldn't even have to think about it and could continue doing things the "old way" if they wanted. While this lacks a certain conceptual purity, I think that's much less important than allowing existing code to continue working unmodified and also allowing people to develop libraries that work with all versions of the language, not just the latest.
It seems confusing, and gives unclear guidiance to newcomers to Go 2.0, to continue to support both the error interface model and a new monadic maybe type.
Them's the brakes: either leave the language frozen as-is, or evolve the language, adding incidental complexity and relegating previous ways of doing things to legacy warts. I really think those are the only two options as adding a new feature which replaces an old one, whether the old feature is deprecated-but-compatible or out the door in the form of a breaking change, is something I think users of the language will have to learn about regardless.
I don't think it's possible to change the language but have newcomers avoid learning both the "old way" and "new way" of doing things, even if Go2 were hypothetically to adopt this outright. You'd still be left with a Go1 and Go2 schism, and newcomers will wonder what the differences are and will inevitably end up having to learn "Go1" anyway.
I think backwards compatibility is helpful both for teaching the language and code compatibility: All existing materials teaching Go will continue to be valid, even if the syntax is outdated. There won't be a need to go through every bit of Go teaching material and invalidate the old syntax: teaching material could, at its leisure, add a notice that there's a new syntax.
I understand "There Is More Than One Way To Do it" generally goes against the Go philosophy of simplicity and minimalism, but is the price that must be paid for adding new language features. New language features will, by their nature, obsolete older approaches.
I'm certainly willing to admit that there might be a way of solving the same core problem in a way that's more natural for Gophers, though, and not such a jarring change from the existing approach.
One more thing to consider: while Go has done an exemplary job of keeping the language easy-to-learn, that isn't the only obstacle involved in onboarding people to a language. I think it's safe to say there are a number of people who look at the verbosity of Go's error handling and are put off by it, some to the point they refuse to adopt the language.
I think it's worth asking whether improvements to the language could attract people who are presently put off by it, and how this balances with making the language harder to learn.
Doing something like monadic error handling goes against Go's philosophy of making you think about errors, however. Monadic error handling and Java-style exception handling are pretty close in semantics (though differnet in syntax). Go took a deliberately different philosophy of expecting the programmer to explicitly handle each error, rather than only adding error handling code when you think of it. In fact, the return nil, err
idiom is strictly speaking not optimal because you can probably add additional useful context.
I feel that any attempts to address Go error handling should bear this in mind, and not make it easy to avoid thinking about errors.
@alercah I pretty much have to beg to differ with everything you've just said...
Doing something like monadic error handling goes against Go's philosophy of making you think about errors
Coming from Rust, I think Rust (or rather, the Rust compiler) actually makes me think about errors more than Go. Rust has a #[must_use] attribute on its Result
type which means unused results generate a compiler warning. This is not so in Go (Rebecca Skinner addresses this in her talk): the Go compiler will not warn for e.g. unhandled error
values.
The Rust type system enforces every error case is addressed in your code, and if not, it's a type error or, at best, a warning.
Monadic error handling and Java-style exception handling are pretty close in semantics (though differnet in syntax).
Let me break down why this isn't true:
(success, error)
Result
sum type e.g. Result<Success, Error>
All-in-all I feel like Go is much closer to Rust than it is to Java when it comes to error handling: errors in Go and Rust are just values, they are not exceptions. You have to opt-in to propagation explicitly. You must convert errors of a different type to the one a given function returns, e.g. through wrapping. They both ultimately represent a success value / error pair, just using different type system features (tuples versus generic sum types).
There are some exceptions where Rust does provide some abstractions that can be electively used on a crate-by-crate basis to do implicit error handling (or rather, explicit error conversion, you still have to manually propagate the error). For example the From
trait can be used to automatically convert errors from one type to another. I personally think being able to define a policy that's completely scoped to a particular package that lets you automatically convert errors from one explicit type to another is an advantage, and not a drawback. Rust's trait system only allows you to define From
for types in your own crate, preventing any sort of spooky-action-at-a-distance.
That's well outside of the scope of this proposal though, and involves several language features Go does not have working in tandem, so I don't think there's any sort of slippery slope where Go is "at risk" of supporting these types of implicit conversions, at least not until Go adds generics and traits/typeclasses.
To toss in my two cents on this matter. I think this sort of functionality would be very useful for companies (such my own employer) where single applications talk to large numbers of subsidiary data sources and compose results in straight-forward fashion.
Here's a representative data sample of some code flow we would have
func generateUser(userID : string) (User, error) {
siteProperties, err := clients.GetSiteProperties()
if err != nil {
return nil, err
}
chatProperties, err := clients.GetChatProperties()
if err != nil {
return nil, err
}
followersProperties, err := clients.GetFollowersProperties()
if err != nil {
return nil, err
}
// ... (repeat X5)
return createUser(siteProperties, ChatProperties, followersProperties, ... /*other properties here */), nil
}
I understand a lot of the pushback that Go is designed to force a user to think about errors at each point, but in codebases where the vast majority of functions return T, err
, this leads to substantial, real-world code bloat and has actually led to production failures because someone forgets to add error handling code after making an additional function call, and the err silently goes unchecked. Further, It's not rare in fact for some of our most chatty services to be ~20%+ error handling, with very little of it being interesting.
Moreover, the vast majority of this error handling logic is identical, and paradoxically the sheer amount of explicit error-handling in our codebases makes it hard to find code where the exceptional case is actually interesting because there's a bit of a 'needle in the haystack' phenomena at play.
I can definitely see why this proposal in particular may not be the solution, but I do believe there needs to be _some_ way of cutting down on this boilerplate.
Some more idle thoughts:
Rust's trailing ?
is a nice syntax. For Go, given the importance of error context, however, I would maybe suggest the following variation:
?
works like Rust, modified for Go. Specifically: it can only be used in a function whose last return value is type error
, and it must appear immediately after a function call whose last return value is also type error
(note: we could allow any type that implements error
as well, but requiring error
prevents the nil interface problem from arising, which is a nice bonus). The effect is that if the error value is non-nil, the function that ?
appears in returns from the function, setting the last parameter to the error value. For functions using named return values, it could either return zeroes for the other values or whatever value is currently stored; for functions that don't, the other return values are always zero..?("opening %s", file)
works as the above, except that the rather than returning the error unmodified, it is passed through a function which composes the errors; roughly speaking, .?(str, vals...)
mutates the erorr like fmt.Errorf(str + ": %s", vals..., err)
.?
syntax or a different one, covering the case where a package wants to export a distinguished error type.Related to #19412 (sum types) and #21161 (error handling) and #15292 (generics).
Related:
"Draft Designs" for new error handling features:
https://go.googlesource.com/proposal/+/master/design/go2draft.md
Feedback re the errors design:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback
I like @alercah suggestion to solve jus this one annoying feature of go-lang that @LegoRemix is talking about, instead of creating separate return type.
I'd just suggest to follow Rust's RFC even more to avoid guessing zero values and introduce catch
expression to let function to specify explicitly what is returned in case main body returns an error:
So this:
func generateUser(userID string) (*User, error) {
siteProperties, err := clients.GetSiteProperties()
if err != nil {
return nil, errors.Wrapf(err, "error generating user: %s", userID)
}
chatProperties, err := clients.GetChatProperties()
if err != nil {
return nil, errors.Wrapf(err, "error generating user: %s", userID)
}
followersProperties, err := clients.GetFollowersProperties()
if err != nil {
return nil, errors.Wrapf(err, "error generating user: %s", userID)
}
return createUser(siteProperties, ChatProperties, followersProperties), nil
}
Becomes this DRY code:
func generateUser(userID string) (*User, error) {
siteProperties := clients.GetSiteProperties()?
chatProperties := clients.GetChatProperties()?
followersProperties := clients.GetFollowersProperties()?
return createUser(siteProperties, ChatProperties, followersProperties), nil
} catch (err error) {
return nil, errors.Wrapf(err, "error generating user: %s", userID)
}
And require that function that is using ?
operator must also define catch
@bradfitz @peterbourgon @SamWhited Maybe there should be another issue for this?
@sheerun Your ?
operator and your catch
statement look very similar to the check
operator and the handle
statement in the new error handling draft design (https://go.googlesource.com/proposal/+/master/design/go2draft.md).
It looks even better, for curious people this is how my code would look like with check
and handle
:
func generateUser(userID string) (*User, error) {
handle err { return nil, errors.Wrapf(err, "error generating user: %s", userID) }
siteProperties := check clients.GetSiteProperties()
chatProperties := check clients.GetChatProperties()
followersProperties := check clients.GetFollowersProperties()
return createUser(siteProperties, chatProperties, followersProperties), nil
}
The only thing I'd change is to get rid of implicit handle
and require it do be defined if check is used. It'll prevent developers from lazily using check and thinking more how to handle or wrap error. The implicit return should be separate feature and could be used as proposed before:
func generateUser(userID string) (*User, error) {
handle err { return _, errors.Wrapf(err, "error generating user: %s", userID) }
siteProperties := check clients.GetSiteProperties()
chatProperties := check clients.GetChatProperties()
followersProperties := check clients.GetFollowersProperties()
return createUser(siteProperties, chatProperties, followersProperties), nil
}
As the author of this proposal, I think it's worth noting that it is effectively invalidated by #15292 and work like https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md, as this proposal was written assuming generic programming facilities are not available. As such, it suggests new syntax to allow for type polymorphism for the special case of result()
, and if that can be avoided by using e.g. contracts I don't think this proposal makes sense anymore.
Since it looks like at least one of those is likely to wind up in Go 2, I'm wondering if this particular proposal should be closed, and if people are still interested in a result type as an alternative to handle
, that it be rewritten assuming e.g. contracts are available.
(Note that I probably don't have time to do that work, but if someone else is interested in seeing this idea forward, go for it)
@sheerun the place to file feedback & ideas on Go 2 error handling is this wiki page:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback
and/or this comprehensive listing of _Requirements to Consider for Go 2 Error Handling:_
https://gist.github.com/networkimprov/961c9caa2631ad3b95413f7d44a2c98a
Most helpful comment
I think this wouldn't be the right direction for Go.
()) })!
, seriously? The main goal of Go should be ease of learning, readability and ease of use. This doesn't help.