proposal: spec: add built-in result type (like Rust, OCaml) #19991

Open
tarcieri opened this Issue Apr 15, 2017 · 73 comments

Comments

Projects
None yet
@tarcieri

tarcieri commented Apr 15, 2017

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.

Background

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.

Goals

I believe the addition of a Result Type to Go could have the following positive outcomes:

  1. Reduce error handling boilerplate: this is an extremely common complaint about Go. The 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.
  2. Allows the compiler to reason about results: in Rust, unconsumed results issue a warning. Though there are linting tools for Go to accomplish the same thing, I think it'd be much more valuable for this to be a first-class feature of the compiler. It's also a reasonably simple one to implement and shouldn't adversely affect compiler performance.
  3. Error handling combinators (this is the part I feel goes against the grain of the language): If there were a type for results, it could support a number of methods for handling, transforming, and consuming results. 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.

Syntax Examples

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.

Result type signature

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)
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?):

return Ok(value)
return Err(value)

Propagating errors

Rust recently added a ? operator for propagating errors (see Rust RFC 243). 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:

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)
count := fd.Write(bytes)!
count := fd.Write?(bytes)
count := fd.Write(bytes)?
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)!

Backwards compatibility

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.

Combinators

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 type

Let'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 values

Often 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.

And more!

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:

https://doc.rust-lang.org/std/result/enum.Result.html

@gopherbot gopherbot added this to the Proposal milestone Apr 15, 2017

@gopherbot gopherbot added the Proposal label Apr 15, 2017

@bradfitz

This comment has been minimized.

Show comment
Hide comment
@bradfitz

bradfitz Apr 15, 2017

Member

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.

Member

bradfitz commented Apr 15, 2017

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.

@Maplicant

This comment has been minimized.

Show comment
Hide comment
@Maplicant

Maplicant Apr 16, 2017

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.

Maplicant commented Apr 16, 2017

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.

@hectorj

This comment has been minimized.

Show comment
Hide comment
@hectorj

hectorj Apr 16, 2017

As someone said in the reddit thread: would definitely prefer proper sum types and generics rather than new special builtins.

hectorj commented Apr 16, 2017

As someone said in the reddit thread: would definitely prefer proper sum types and generics rather than new special builtins.

@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri Apr 16, 2017

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).

tarcieri commented Apr 16, 2017

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).

@ianlancetaylor

This comment has been minimized.

Show comment
Hide comment
@ianlancetaylor

ianlancetaylor Apr 16, 2017

Contributor

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.

Contributor

ianlancetaylor commented Apr 16, 2017

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.

@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri Apr 16, 2017

@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)

@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)

@ianlancetaylor

This comment has been minimized.

Show comment
Hide comment
@ianlancetaylor

ianlancetaylor Apr 17, 2017

Contributor

@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.

Contributor

ianlancetaylor commented Apr 17, 2017

@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.

@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri Apr 17, 2017

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<int, error>

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 commented Apr 17, 2017

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<int, error>

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.

@griesemer

This comment has been minimized.

Show comment
Hide comment
@griesemer

griesemer Apr 17, 2017

Contributor

@tarcieri What's wrong with a struct? https://play.golang.org/p/mTqtaMbgIF

Contributor

griesemer commented Apr 17, 2017

@tarcieri What's wrong with a struct? https://play.golang.org/p/mTqtaMbgIF

@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri Apr 17, 2017

@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.

@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.

@griesemer

This comment has been minimized.

Show comment
Hide comment
@griesemer

griesemer Apr 17, 2017

Contributor

@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.

Contributor

griesemer commented Apr 17, 2017

@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.

@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri Apr 17, 2017

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.

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.

@griesemer

This comment has been minimized.

Show comment
Hide comment
@griesemer

griesemer Apr 17, 2017

Contributor

@tarcieri Composition as you suggested would also be possible with a single result struct type.

Contributor

griesemer commented Apr 17, 2017

@tarcieri Composition as you suggested would also be possible with a single result struct type.

@garethwarry

This comment has been minimized.

Show comment
Hide comment
@garethwarry

garethwarry Apr 18, 2017

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? 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.

@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri Apr 19, 2017

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.

tarcieri commented Apr 19, 2017

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.

@as

This comment has been minimized.

Show comment
Hide comment
@as

as Apr 19, 2017

Contributor

Thank you for sharing your ideas, but I think the examples used above are unconvincing. To me, A is better than B:

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
}

B

result := doThing(a).
	Then(func(resp) { doAnotherThing(b, resp.foo()) }).
	Then(func(resp) { FinishUp(c, resp.bar()) })

if result.isError() {
	return result.Error()
}
  • A is more readable, out loud and mentally.
  • A doesn't require line formatting/wrapping
  • In A, the error conditions terminate execution, explicitly; requiring no mental negation. B is not similar.
  • In B, the keyword "Then" does not indicate conditional causality. The keyword "if" does, and it is already in the language.
  • In B, I don't want to slow down my most likely branch of execution by packing it into a lambda
Contributor

as commented Apr 19, 2017

Thank you for sharing your ideas, but I think the examples used above are unconvincing. To me, A is better than B:

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
}

B

result := doThing(a).
	Then(func(resp) { doAnotherThing(b, resp.foo()) }).
	Then(func(resp) { FinishUp(c, resp.bar()) })

if result.isError() {
	return result.Error()
}
  • A is more readable, out loud and mentally.
  • A doesn't require line formatting/wrapping
  • In A, the error conditions terminate execution, explicitly; requiring no mental negation. B is not similar.
  • In B, the keyword "Then" does not indicate conditional causality. The keyword "if" does, and it is already in the language.
  • In B, I don't want to slow down my most likely branch of execution by packing it into a lambda
@urandom

This comment has been minimized.

Show comment
Hide comment
@urandom

urandom Apr 21, 2017

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.

urandom commented Apr 21, 2017

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.

@iporsut

This comment has been minimized.

Show comment
Hide comment
@iporsut

iporsut Apr 28, 2017

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
}

iporsut commented Apr 28, 2017

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
}
@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri Apr 28, 2017

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.

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.

@as

This comment has been minimized.

Show comment
Hide comment
@as

as Apr 29, 2017

Contributor

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.

Contributor

as commented Apr 29, 2017

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.

@jredville

This comment has been minimized.

Show comment
Hide comment
@jredville

jredville Apr 30, 2017

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.

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.

@as

This comment has been minimized.

Show comment
Hide comment
@as

as May 1, 2017

Contributor

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.

Contributor

as commented May 1, 2017

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.

@creker

This comment has been minimized.

Show comment
Hide comment
@creker

creker May 1, 2017

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.

creker commented May 1, 2017

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.

@jredville

This comment has been minimized.

Show comment
Hide comment
@jredville

jredville May 1, 2017

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.

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.

@kr

This comment has been minimized.

Show comment
Hide comment
@kr

kr May 1, 2017

Contributor

I don't fully understand how this proposal would achieve its stated goals.

  1. 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.

  2. 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.

  3. Error handling combinators: this goal is certainly achieved by the proposal, but it is not entirely clear that it would be worth the cost of the necessary changes to achieve it, in the context of the Go language as it stands today. (I think this is the main point of contention in the discussion so far.)

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.

Contributor

kr commented May 1, 2017

I don't fully understand how this proposal would achieve its stated goals.

  1. 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.

  2. 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.

  3. Error handling combinators: this goal is certainly achieved by the proposal, but it is not entirely clear that it would be worth the cost of the necessary changes to achieve it, in the context of the Go language as it stands today. (I think this is the main point of contention in the discussion so far.)

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.

@peterbourgon

This comment has been minimized.

Show comment
Hide comment
@peterbourgon

peterbourgon May 1, 2017

Member

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)
}
Member

peterbourgon commented May 1, 2017

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)
}
@urandom

This comment has been minimized.

Show comment
Hide comment
@urandom

urandom May 1, 2017

@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 commented May 1, 2017

@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.

@iporsut

This comment has been minimized.

Show comment
Hide comment
@iporsut

iporsut May 2, 2017

@urandom I think result is pair of (val, error) so I think checks the error/returns are the main characters in the plot too.

iporsut commented May 2, 2017

@urandom I think result is pair of (val, error) so I think checks the error/returns are the main characters in the plot too.

@novikk

This comment has been minimized.

Show comment
Hide comment
@novikk

novikk May 2, 2017

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).

novikk commented May 2, 2017

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).

@egorse

This comment has been minimized.

Show comment
Hide comment
@egorse

egorse May 2, 2017

Sounds more and more as #18721

egorse commented May 2, 2017

Sounds more and more as #18721

@iporsut

This comment has been minimized.

Show comment
Hide comment
@iporsut

iporsut May 3, 2017

@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 commented May 3, 2017

@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)
}
@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri May 3, 2017

@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:

  1. No type safety: with reflection we cannot determine at compile time if the closure is suitably typed. Instead our program will compile regardless of the types, and we'll encounter a runtime crash if they're mismatched.
  2. Huge performance overhead: the approach you're suggesting is not too far off from the one offered by go-linq. They claim using reflection for this purpose is "5x-10x slower". Now imagine this amount of overhead at every single call site.

To me either of these problems are a huge step backward from what Go has already, and in tandem they're a complete nonstarter.

tarcieri commented May 3, 2017

@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:

  1. No type safety: with reflection we cannot determine at compile time if the closure is suitably typed. Instead our program will compile regardless of the types, and we'll encounter a runtime crash if they're mismatched.
  2. Huge performance overhead: the approach you're suggesting is not too far off from the one offered by go-linq. They claim using reflection for this purpose is "5x-10x slower". Now imagine this amount of overhead at every single call site.

To me either of these problems are a huge step backward from what Go has already, and in tandem they're a complete nonstarter.

@Kiura

This comment has been minimized.

Show comment
Hide comment
@Kiura

Kiura May 7, 2017

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"

Kiura commented May 7, 2017

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"

@Kiura

This comment has been minimized.

Show comment
Hide comment
@Kiura

Kiura May 7, 2017

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 ))

Kiura commented May 7, 2017

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 ))

@alercah

This comment has been minimized.

Show comment
Hide comment
@alercah

alercah May 10, 2017

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:

  1. It is treated like it has type error for the purposes of assignment.
  2. If nil is assigned to it, nothing happens.
  3. If a non-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.

alercah commented May 10, 2017

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:

  1. It is treated like it has type error for the purposes of assignment.
  2. If nil is assigned to it, nothing happens.
  3. If a non-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.

@billyh

This comment has been minimized.

Show comment
Hide comment
@billyh

billyh May 15, 2017

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:

  1. Reduces repetitive error handling boilerplate and maximizes focus on the primary intent of the code path.
  2. Encourages proper error handling, including wrapping of errors when propagating them onward.
  3. Adheres to the Go design principles of clarity and simplicity.
  4. Is applicable in the broadest possible range of error handling situations.

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:

  1. Close to minimum additional boilerplate for error handling.
  2. Improves focus on the primary intent of the code with minimal error handling baggage on the left side of the statement and error handling localized on the right side.
  3. Works in many different situations giving the programmer flexibility in the case of multiple return values (e.g. if you want to return an indicator of the count of items that succeeded in addition to the error).
  4. Syntax is simple and would be easily understood and adopted by Go users, both new and old.
  5. Partially succeeds in encouraging proper error handling by making error code more succinct. May make error code slightly less likely to be copy-pasted and thereby reduce introduction of common copy-paste errors.

Disadvantages:

  1. This approach doesn't fully succeed in encouraging proper error handling because it does nothing to promote wrapping errors before propagating them. In my ideal world, this new syntax would have required that the error returned by 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.
  2. Some may argue that this is all syntactic sugar and not needed in the language. A counter argument might be that the current error handling in Go is syntactic trans fat, and this proposal just eliminates it. To be widely adopted, a programming language should be pleasurable to use. Largely Go succeeds at that, but the error handling boilerplate is a particularly profuse exception.
  3. Are we "catching" the error from the function we call, or are we "throwing" an error to whoever called us? Is it appropriate to have a 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.

billyh commented May 15, 2017

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:

  1. Reduces repetitive error handling boilerplate and maximizes focus on the primary intent of the code path.
  2. Encourages proper error handling, including wrapping of errors when propagating them onward.
  3. Adheres to the Go design principles of clarity and simplicity.
  4. Is applicable in the broadest possible range of error handling situations.

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:

  1. Close to minimum additional boilerplate for error handling.
  2. Improves focus on the primary intent of the code with minimal error handling baggage on the left side of the statement and error handling localized on the right side.
  3. Works in many different situations giving the programmer flexibility in the case of multiple return values (e.g. if you want to return an indicator of the count of items that succeeded in addition to the error).
  4. Syntax is simple and would be easily understood and adopted by Go users, both new and old.
  5. Partially succeeds in encouraging proper error handling by making error code more succinct. May make error code slightly less likely to be copy-pasted and thereby reduce introduction of common copy-paste errors.

Disadvantages:

  1. This approach doesn't fully succeed in encouraging proper error handling because it does nothing to promote wrapping errors before propagating them. In my ideal world, this new syntax would have required that the error returned by 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.
  2. Some may argue that this is all syntactic sugar and not needed in the language. A counter argument might be that the current error handling in Go is syntactic trans fat, and this proposal just eliminates it. To be widely adopted, a programming language should be pleasurable to use. Largely Go succeeds at that, but the error handling boilerplate is a particularly profuse exception.
  3. Are we "catching" the error from the function we call, or are we "throwing" an error to whoever called us? Is it appropriate to have a 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.
@billyh

This comment has been minimized.

Show comment
Hide comment
@billyh

billyh May 15, 2017

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?

billyh commented May 15, 2017

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?

@Kiura

This comment has been minimized.

Show comment
Hide comment
@Kiura

Kiura May 15, 2017

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

Kiura commented May 15, 2017

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

@cznic

This comment has been minimized.

Show comment
Hide comment
@cznic

cznic May 15, 2017

Contributor

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.

Contributor

cznic commented May 15, 2017

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.

@alercah

This comment has been minimized.

Show comment
Hide comment
@alercah

alercah May 15, 2017

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.

alercah commented May 15, 2017

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.

@urandom

This comment has been minimized.

Show comment
Hide comment
@urandom

urandom May 16, 2017

@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.

urandom commented May 16, 2017

@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.

@davecheney

This comment has been minimized.

Show comment
Hide comment
@davecheney

davecheney May 16, 2017

Contributor
Contributor

davecheney commented May 16, 2017

@billyh

This comment has been minimized.

Show comment
Hide comment
@billyh

billyh May 16, 2017

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.

billyh commented May 16, 2017

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.

@davecheney

This comment has been minimized.

Show comment
Hide comment
@davecheney

davecheney May 16, 2017

Contributor
Contributor

davecheney commented May 16, 2017

@billyh

This comment has been minimized.

Show comment
Hide comment
@billyh

billyh May 16, 2017

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.

billyh commented May 16, 2017

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.

@davecheney

This comment has been minimized.

Show comment
Hide comment
@davecheney

davecheney May 16, 2017

Contributor
Contributor

davecheney commented May 16, 2017

@creker

This comment has been minimized.

Show comment
Hide comment
@creker

creker May 16, 2017

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.

creker commented May 16, 2017

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.

@billyh

This comment has been minimized.

Show comment
Hide comment
@billyh

billyh May 16, 2017

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.

billyh commented May 16, 2017

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.

@davecheney

This comment has been minimized.

Show comment
Hide comment
@davecheney

davecheney May 16, 2017

Contributor
Contributor

davecheney commented May 16, 2017

@Kiura

This comment has been minimized.

Show comment
Hide comment
@Kiura

Kiura May 16, 2017

@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.

Kiura commented May 16, 2017

@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.

@SamWhited

This comment has been minimized.

Show comment
Hide comment
@SamWhited

SamWhited May 16, 2017

Member

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).

Member

SamWhited commented May 16, 2017

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).

@davecheney

This comment has been minimized.

Show comment
Hide comment
@davecheney

davecheney May 16, 2017

Contributor
Contributor

davecheney commented May 16, 2017

@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri May 16, 2017

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.

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.

@Kiura

This comment has been minimized.

Show comment
Hide comment
@Kiura

Kiura May 16, 2017

@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.

Kiura commented May 16, 2017

@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.

@SamWhited

This comment has been minimized.

Show comment
Hide comment
@SamWhited

SamWhited May 16, 2017

Member

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) :)

Member

SamWhited commented May 16, 2017

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) :)

@Kiura

This comment has been minimized.

Show comment
Hide comment
@Kiura

Kiura May 16, 2017

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.

Kiura commented May 16, 2017

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.

@alercah

This comment has been minimized.

Show comment
Hide comment
@alercah

alercah May 16, 2017

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.

alercah commented May 16, 2017

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.

@rsc rsc changed the title from proposal: Go 2: "result" type to proposal: spec: add built-in result type (like Rust, OCaml) Jun 16, 2017

@egonelbre

This comment has been minimized.

Show comment
Hide comment
@egonelbre

egonelbre Jun 18, 2017

Contributor

@billyh With regards to the "Error handling patterns in Go" article, there are other solutions:

Contributor

egonelbre commented Jun 18, 2017

@billyh With regards to the "Error handling patterns in Go" article, there are other solutions:

@urandom

This comment has been minimized.

Show comment
Hide comment
@urandom

urandom Jun 18, 2017

@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 commented Jun 18, 2017

@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.

@egonelbre

This comment has been minimized.

Show comment
Hide comment
@egonelbre

egonelbre Jun 18, 2017

Contributor

@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.

Contributor

egonelbre commented Jun 18, 2017

@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.

@urandom

This comment has been minimized.

Show comment
Hide comment
@urandom

urandom Jun 19, 2017

@egonelbre

https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go

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 commented Jun 19, 2017

@egonelbre

https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go

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.

@egonelbre

This comment has been minimized.

Show comment
Hide comment
@egonelbre

egonelbre Jun 19, 2017

Contributor

@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:

  • good error messages
  • recovery / alternate paths based on error value
  • fallbacks
  • best effort execution with errors
  • happy-case logging
  • failure logging
  • failure tracing
  • multiple errors being returned
  • of course, some of these will be used together
  • ... probably some that I missed ...

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.

Contributor

egonelbre commented Jun 19, 2017

@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:

  • good error messages
  • recovery / alternate paths based on error value
  • fallbacks
  • best effort execution with errors
  • happy-case logging
  • failure logging
  • failure tracing
  • multiple errors being returned
  • of course, some of these will be used together
  • ... probably some that I missed ...

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.

@billyh

This comment has been minimized.

Show comment
Hide comment
@billyh

billyh Jun 24, 2017

@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.

billyh commented Jun 24, 2017

@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.

@billyh

This comment has been minimized.

Show comment
Hide comment
@billyh

billyh Jun 25, 2017

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.)

billyh commented Jun 25, 2017

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.)

@cznic

This comment has been minimized.

Show comment
Hide comment
@cznic

cznic Jun 25, 2017

Contributor

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.

Contributor

cznic commented Jun 25, 2017

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.

@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri Jun 25, 2017

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.

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.

@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri Jun 25, 2017

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.

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.

@alercah

This comment has been minimized.

Show comment
Hide comment
@alercah

alercah Jun 25, 2017

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 commented Jun 25, 2017

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.

@tarcieri

This comment has been minimized.

Show comment
Hide comment
@tarcieri

tarcieri Jun 25, 2017

@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:

Error Propagation Strategy

  • Go: return value, explicitly propagated
  • Java: non-local jump, implicitly propagated
  • Rust: return value, explicitly propagated

Error Types

  • Go: one return value per function, generally 2-tuple of (success, error)
  • Java: checked exceptions consisting of many exception types, representing the set union of all exceptions potentially thrown from a method. Also unchecked exceptions that aren't declared and can happen anywhere at any time.
  • Rust: one return value per function, generally 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.

tarcieri commented Jun 25, 2017

@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:

Error Propagation Strategy

  • Go: return value, explicitly propagated
  • Java: non-local jump, implicitly propagated
  • Rust: return value, explicitly propagated

Error Types

  • Go: one return value per function, generally 2-tuple of (success, error)
  • Java: checked exceptions consisting of many exception types, representing the set union of all exceptions potentially thrown from a method. Also unchecked exceptions that aren't declared and can happen anywhere at any time.
  • Rust: one return value per function, generally 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.

@LegoRemix

This comment has been minimized.

Show comment
Hide comment
@LegoRemix

LegoRemix Aug 21, 2017

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.

LegoRemix commented Aug 21, 2017

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.

@alercah

This comment has been minimized.

Show comment
Hide comment
@alercah

alercah Aug 22, 2017

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:

  • Trailing ? 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.
  • Trailing .?("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)
  • Possibly there should be a version, either a variant of the .? syntax or a different one, covering the case where a package wants to export a distinguished error type.

alercah commented Aug 22, 2017

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:

  • Trailing ? 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.
  • Trailing .?("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)
  • Possibly there should be a version, either a variant of the .? syntax or a different one, covering the case where a package wants to export a distinguished error type.
@ianlancetaylor

This comment has been minimized.

Show comment
Hide comment
@ianlancetaylor

ianlancetaylor Feb 13, 2018

Contributor

Related to #19412 (sum types) and #21161 (error handling) and #15292 (generics).

Contributor

ianlancetaylor commented Feb 13, 2018

Related to #19412 (sum types) and #21161 (error handling) and #15292 (generics).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment