New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
tarcieri opened this Issue Apr 15, 2017 · 79 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.

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.

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.

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.

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.

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.

tarcieri commented 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

This comment has been minimized.

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.

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.

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.

tarcieri commented 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

This comment has been minimized.

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.

tarcieri commented 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.

@griesemer

This comment has been minimized.

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.

garethwarry commented 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.

@tarcieri

This comment has been minimized.

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.

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.

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.

Contributor

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.

tarcieri commented 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.

@as

This comment has been minimized.

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.

jredville commented 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.

@as

This comment has been minimized.

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.

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.

jredville commented 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.

@kr

This comment has been minimized.

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.

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

This comment has been minimized.

tarcieri commented 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.

@Kiura

This comment has been minimized.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

tarcieri commented 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.

@tarcieri

This comment has been minimized.

tarcieri commented 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.

@alercah

This comment has been minimized.

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.

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.

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.

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.

Contributor

ianlancetaylor commented Feb 13, 2018

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

@networkimprov

This comment has been minimized.

networkimprov commented Sep 25, 2018

Related:

"Draft Designs" for new error handling features:
https://go.googlesource.com/proposal/+/master/design/go2draft.md

Feedback re the errors design:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

@sheerun

This comment has been minimized.

sheerun commented Nov 30, 2018

I like @alercah suggestion to solve jus this one annoying feature of go-lang that @LegoRemix is talking about, instead of creating separate return type.

I'd just suggest to follow Rust's RFC even more to avoid guessing zero values and introduce catch expression to let function to specify explicitly what is returned in case main body returns an error:

So this:

func generateUser(userID string) (*User, error) {
    siteProperties, err := clients.GetSiteProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    chatProperties, err := clients.GetChatProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    followersProperties, err := clients.GetFollowersProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    return createUser(siteProperties, ChatProperties, followersProperties), nil
}

Becomes this DRY code:

func generateUser(userID string) (*User, error) {
    siteProperties := clients.GetSiteProperties()?
    chatProperties := clients.GetChatProperties()?
    followersProperties := clients.GetFollowersProperties()?

    return createUser(siteProperties, ChatProperties, followersProperties), nil
} catch (err error) {
    return nil, errors.Wrapf(err, "error generating user: %s", userID)
}

And require that function that is using ? operator must also define catch

@bradfitz @peterbourgon @SamWhited Maybe there should be another issue for this?

@ngrilly

This comment has been minimized.

ngrilly commented Nov 30, 2018

@sheerun Your ?operator and your catch statement look very similar to the check operator and the handle statement in the new error handling draft design (https://go.googlesource.com/proposal/+/master/design/go2draft.md).

@sheerun

This comment has been minimized.

sheerun commented Nov 30, 2018

It looks even better, for curious people this is how my code would look like with check and handle:

func generateUser(userID string) (*User, error) {
    handle err { return nil, errors.Wrapf(err, "error generating user: %s", userID) }

    siteProperties := check clients.GetSiteProperties()
    chatProperties := check clients.GetChatProperties()
    followersProperties := check clients.GetFollowersProperties()

    return createUser(siteProperties, chatProperties, followersProperties), nil
}

The only thing I'd change is to get rid of implicit handle and require it do be defined if check is used. It'll prevent developers from lazily using check and thinking more how to handle or wrap error. The implicit return should be separate feature and could be used as proposed before:

func generateUser(userID string) (*User, error) {
    handle err { return _, errors.Wrapf(err, "error generating user: %s", userID) }

    siteProperties := check clients.GetSiteProperties()
    chatProperties := check clients.GetChatProperties()
    followersProperties := check clients.GetFollowersProperties()

    return createUser(siteProperties, chatProperties, followersProperties), nil
}
@tarcieri

This comment has been minimized.

tarcieri commented Nov 30, 2018

As the author of this proposal, I think it's worth noting that it is effectively invalidated by #15292 and work like https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md, as this proposal was written assuming generic programming facilities are not available. As such, it suggests new syntax to allow for type polymorphism for the special case of result(), and if that can be avoided by using e.g. contracts I don't think this proposal makes sense anymore.

Since it looks like at least one of those is likely to wind up in Go 2, I'm wondering if this particular proposal should be closed, and if people are still interested in a result type as an alternative to handle, that it be rewritten assuming e.g. contracts are available.

(Note that I probably don't have time to do that work, but if someone else is interested in seeing this idea forward, go for it)

@networkimprov

This comment has been minimized.

networkimprov commented Nov 30, 2018

@sheerun the place to file feedback & ideas on Go 2 error handling is this wiki page:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

and/or this comprehensive listing of Requirements to Consider for Go 2 Error Handling:
https://gist.github.com/networkimprov/961c9caa2631ad3b95413f7d44a2c98a

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