golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.83k stars 17.51k forks source link

Proposal: A built-in go error check function, "catch" #32811

Closed natefinch closed 5 years ago

natefinch commented 5 years ago

This is a counter-proposal to #32437

Proposal: A built-in Go error check function, catch

catch would function much like the proposed try with a few specific differences:

1.) catch would not return any values, meaning it must be on a line by itself, like panic() 2.) catch takes 1..N arguments 2a.) the first argument must be of type error 2b.) The remainder of the arguments are optional and wrap the error using the given format string and args, as if sent through the new fmt.Errorf which does error wrapping similar to github.com/pkg/errors.

e.g.

func getConfig(config string) (*Config, error)
    f, err := os.Open(config)
    catch(err)
    defer f.Close()
    // use f to make a c *Config...
    return c, nil
}

In this code, catch is the equivalent of

if err != nil {
    return nil, err
}

If err is non-nil, it will return zero values for all other return values, just like try. The difference being that since catch doesn't return values, you can't "hide" it on the right hand side. You also can't nest catch inside another function, and the only function you can nest inside of catch is one that just returns an error. This is to ensure readability of the code.

This makes catch just as easy to see in the flow of the code as if err != nil is now. It means you can't magically exit from a function in the middle of a line if something fails. It removes nesting of functions which is otherwise usually rare and discouraged in go code, for readability reasons.

This almost makes catch like a keyword, except it's backwards compatible with existing code in case someone already has the name catch defined in their code (though I think others have done homework saying try and catch are both rarely used names in Go code).

Optionally, you can add more data to an error in the same line as catch:

func getConfig(user, config string) (*Config, error)
    f, err := os.Open(config)
    catch(err, "can't open config for user %s", user)
    defer f.Close()
    // use f to make a c * Config...
    return c, nil
}

In this configuration, catch is equivalent to

if err != nil {
    return nil, fmt.Errorf("can't open config for user %s: %v", user, err)
}

And would utilize the new implicit error wrapping.

This proposal accomplishes 3 things:

1.) It reduces if err != nil boilerplate 2.) It maintains current legibility of exit points of a function having to be indicated by the first word on the line 3.) It makes it easier to annotate errors than either if err != nil or the try proposal.

natefinch commented 5 years ago

For the record, I don't mind if err != nil, and all things being equal, I would rather not remove that boilerplate, since errors are just values and all that jazz. I know many, many long-time gophers that agree.

However, if we're going to change the language, this is a much better change than the proposed try in my opinion. This maintains legibility of code, maintains ease of finding exit points in the code, still reduces boilerplate, and encourages people to add context to an error.

IMO it removes the major faults of try, which are the high likelihood of missing a call to try embedded in the right hand side somewhere when reading code, and the problem of encouraging nesting of functions which is distinctly bad for code readability.

thepudds commented 5 years ago

One variation would be:

  decorate := func(err error) error { return fmt.Errorf("foo failed: %v", err) }

  ...

  f, err := os.Open(config)
  catch(err, decorate)          // decorate called in error case

or using helper functions that could live somewhere in the standard library:

  catch(err, fmt.Annotatef("foo failed for %v", arg1))      

That would be more parallel to the current proposal for try as a builtin.

edit: part of the rationale for a handler function is it means a builtin does not directly rely on fmt semantics, which is the concern @Merovius expressed better in the next comment.

Merovius commented 5 years ago

My main reason for disagreeing with that is that it couples the fmt-package into the language. i.e. to actually add this to the spec, you'd also need to define what fmt does in the spec - and IMO, fmt is far too large (conceptually) to be part of the language proper. So, IMO, the multiple-argument form you are suggesting should be removed.

natefinch commented 5 years ago

That's a good point, and actually why I made 2a and 2b. I think we could remove 2b pretty easily. It could easily be replaced by a helper function in fmt (or user code):

// Wrap returns nil if err is nil, otherwise it returns a wrapped 
// version of err with the given fmt string as per fmt.Errorf.
func Wrap(err error, msg string, args ...interface{})

then you could do

catch(fmt.Wrap(err, "error opening config for user %s", user))

It's not quite as nice, though.

And for the record, I'd be ok with making this part of fmt be part of the language if it encourages people annotate their errors.

rof20004 commented 5 years ago

@natefinch What about if i have more than two returns?

func getConfig(user, config string) (*Config, error, result)
    f, err := os.Open(config)
    catch(err)
    defer f.Close()
    // use f to make a c * Config...
    return c, nil, nil
}
natefinch commented 5 years ago

It works like try from the linked issue, where using it requires that it's in a function where the last return value is of type error, and it returns zero values for everything except the error.

so like if you had

func getConfig(user, config string) (*Config, result, error)

catch(err) would return nil, result{}, err

bakul commented 5 years ago

You can simplify this a bit. Since it expects an error value, you can give it any error value. So then it can be catch err or catch fmt.Errorf(“can’t open config for %s”, user) — no need to implicitly use this error generating function and you can return any kind of error that makes sense in your situation. Though I don’t like the use of catch. It would be perfectly legal in your scheme to say something like this but it looks weird!

    If !inrange(low, value, high) {
        catch OutOfRangeError
    }

Except for the name, this would be a perfectly legitimate use at the where error conditions are first detected and even better than having to say

    If !inrange(low, value, high) {
        return nil, OutOfRangeError
    }

On IBM systems on can use a macro ABEND to abnormally terminate a task. It was a bit like panic but the name was suggestive of its intended use. catch doesn’t have that. Neither did try. I thought of returnOnError or errorReturn or just ereturn or abreturn or may be even raise as in C. Any of these work for me better than catch or try but none seem ideal.

natefinch commented 5 years ago

So, it can't be a keyword, because that's not backwards compatible. So it would need to be catch(OutOfRangeError) ....

I do agree that the name might need some tweaking. Naming is hard, and I welcome suggestions for another name that might be more appropriate.

The problem is really that it is most accurately called maybeReturn

How about yield? Where it's keep going, or stop if something is in the way? Also yield is to produce, like it produces the error.

f, err := os.Open(config)
yield(err) 
defer f.Close()

and then it could be

yield(OutOfRangeError)

which looks better to me

bakul commented 5 years ago

(Ignoring its use as giving up the processor to another coroutine or thread) yield is better than catch! returnOnError or returnIfError exactly captures the semantics but it is a bit long.

returnIf can also work provided error values have err or error in their name.

returnIf(OutOfRangeError)
provPaulBrousseau commented 5 years ago

Both yield and catch have very strong connotations in other languages, and I wouldn't suggest them for your proposal. As you say, naming is hard.

But I have to disagree with this if for no other reason than its rigidity; if

catch(err, "can't open config for user %s", user)

is really just

if err != nil {
  return fmt.Errorf("can't open config for user %s: %v", user, err)
}

... it's really not that valuable. Most of my error handling is done with github.com/pkg/errors, although I have yet to evaluate the new proposals around wrapped errors. And others perhaps like dedicated typed errors, while others may use constants. This solves a narrow set circumstances.

But to think this through further... what happens in this case:

func DoYourThing(a string) (x string, err error) {
  x = fmt.Sprintf("My thing is '%a'", a)
  // always returns an err
  err = DoSomethingBad() 
  catch(err)

  // process some other stuff.

  return
}

func DoAThing() {
  x, _ := DoYourThing("cooking")
  fmt.Printf("%s!!\n", x)
}
bakul commented 5 years ago

@provPaulBrousseau, IMHO catch or its equivalent shouldn’t be used if you are returning an error as well as a legit value. [This is another reason I bemoan the lack of a sum type in Go. With sum type you’d use it in most cases where an error or a legit value is returned but not both. For examples such as yours, a tuple or a product type would be used. In such a language catch would only work if the parent function returned a sum type]

But if you do use catch, it would return “”, err.

natefinch commented 5 years ago

@provPaulBrousseau note that my intent is that this use the new fmt.Errorf which actually does wrapping like github.com/pkg/errors (which is what we use at work, as well). So that fmt.Errorf looks like the oldschool "just reuse the string", but it's actually wrapping under the hood. I'll update the original post to make that clear.

Certainly, if you want something more complicated, like turning one error into some other type of error, you'd instead use the methods you used before, with an if statement. This is just trying to remove a little boilerplate and remove some friction from adding context to errors.

natefinch commented 5 years ago

oh.... and yes, just like try, catch, and yield have meanings in other languages.... but this isn't other languages, it's Go. And if the name is a good one, reusing it to mean something different seems ok, IMO... especially since it's not a keyword.

freeformz commented 5 years ago

Option 2c?

catch takes an error value. When the error value is nil processing continues. When it's !nil it returns from the enclosing function with zero values for each return parameter except the last error value, for which it uses the supplied error.

If named return parameters are used, then the function returns with those values and the supplied error when the error is not nil.

It's a compile time error to use catch in a function that does not have an error value as it's last return. It's a compile time error to provide more than one argument to catch.

Examples:

func Foo() (int, int, error) {
  err := ....
  catch err // nil processing continues, !nil return 0,0, err

  user := "sally"
  err := ....
  catch fmt.Errorf("can't open config for user %s: %w", user, err) // note new %w flag, so it's an `error`  and an `errors.Wrapper`, nil processing continue, !nil return 0,0, <formatted error>

I like this for all the reasons you pointed out. And there is less "magic" wrt formatting, and if after a release or two it's felt that formatting should be built in it can still be added later by allowing the catch statement to take multiple arguments.

catch <error> makes it look more like a statement than a function, which IMO helps with it standing out visually and is less likely to lead newcomers to think it's a function.

freeformz commented 5 years ago

PS: I realize this would require a tweak to fmt.Errorf to make it return nil if the provided error is nil. So what you said with the Wrap function instead:

func Foo() (int, int, error) {
  user := "sally"
  err := ...
  catch errors.Wrap("can't open config for user: %s: %w", user, err)
apghero commented 5 years ago

catch is a statement, i.e. it must be on a line by itself, like panic()

This seems like a non-starter. Can it just be a func catch(err error, args ...interface{}) instead? That'd be the same result, and backwards compatible. That makes the biggest problem the fact that you need some static analysis to ensure that the return type of the enclosing function returns an error... (not sure how difficult that'd be to achieve in Go's compiler)

freeformz commented 5 years ago

WRT Backwards compatibility (catch <error> isn't backwards compatible)..... I'd be more than happy to wait for something like this until modules becomes the default build mode ala https://blog.golang.org/go2-next-steps

daved commented 5 years ago

At first glance, this is generally what try and check should have been. check is a great name (and way to avoid the baggage of catch), whether meaning "verify" or "halt/slow progress".

natefinch commented 5 years ago

@apghero why is it a nonstarter? That's basically the whole difference between this and the try proposal. I do not want a function on the right hand side of a complicated statement to be able to exit. I want it to have to be the only thing on the line so it is hard to miss.

freeformz commented 5 years ago

@daved check SGTM, just not all the other bits.

ubikenobi commented 5 years ago

This is many ways worse than try() function proposal in my opinion because it doesn't really solve the underlying goal of reducing boilerplate code.

A function with multiple error check looks like this:

func Foo() (err error) {
    var file1, err1 = open("file1.txt")
    catch(err1)
    defer file1.Close()
    var file2, err2 = open("file2.txt")
    catch(err2)
    defer file1.Close()
}

Vs. originally proposed try() function:


func Foo() (err error) {
    var file1 = try(open("file1"))
    defer file1.Close()
    var file2 = try(open("file1"))
    defer file2.Close()
}

Later looks more concise and elegant in my opinion.

daved commented 5 years ago

@ubikenobi It's always trade-offs. In my opinion, the reduction provided by try costs far too much in readability. More so when the extra "flexibility" try permits is weighed. The clarity that comes from that single newline (and possibly two columns of text) is worthwhile. The surety that comes from reduced flexibility? Even more critically so.

apghero commented 5 years ago

@apghero why is it a nonstarter? That's basically the whole difference between this and the try proposal. I do not want a function on the right hand side of a complicated statement to be able to exit. I want it to have to be the only thing on the line so it is hard to miss.

Me either. But that can be accomplished without syntax. The syntax part is the thing that breaks backwards compatibility, so why not just do what panic does and return nothing? You can't use a function that returns nothing on the right hand side of a statement, or in an expression. Therefore it's limited to the left most column of a block, which is what we're looking for.

mathieudevos commented 5 years ago

This still would take up a keyword: catch, but only barely improve on the try variant. While I do think this is a step forward, it still only allows "lazily" error handling.

What for users that use custom errors that use wrapping functions? What for users that want to log for developers & operators and return human-understandable errors to the users?

if err != nil {
    log.Println("Hey operating, big error, here's the stack trace: ...")
    return myerrorpackage.WrappedErrorConstructor(http.StatusInternalServerError, "Some readable message")
}

If we plan on taking up a whole keyword, can we at least attach some function to it? Counter proposal to the whole try or catch: do try/catch with check & handle (better keywords imho), scope specific.

Suggestion is part of the whole mix of error handling proposals: scoped check/handle proposal

daved commented 5 years ago

This still would take up a keyword: catch, but only barely improve on the try variant.

The point is to reduce the footgun rating of a solution, not to offer an "improved" novelty.

What for users that use custom errors that use wrapping functions?

This was covered in comments.

If we plan on taking up a whole keyword, can we at least attach some function to it?

Now I'm not sure if this is a troll post. Most keywords serve exactly one function. return, if, etc.

ngrilly commented 5 years ago

This proposal states several times that catch(...) is a statement, like panic(...). But panic is not a statement, it's a built-in function with no return value.

ubikenobi commented 5 years ago

@daved Inconsistencies and unnecessary code clutter also negatively impact readability in my opinion. This is why I think catch() proposal is less readable and less safe compared to try() proposal. I'll try to illustrate my three main concerns with a different example below.


//Example 1
func Dispense() (err error) {
    var motErr= SetMotorSpeed(5)
    catch(motErr)
    var fanErr= StartFan(5)
    catch(fanErr)
    var tempErr =SetTemperature(200)
    catch(tempErr)
    var itErr = DropItem()
    catch(itErr)
}
//Example 2
func Dispense() (err error)
    var motErr= SetMotorSpeed(5)
    var speedErr= StartFan()
    var tempErr =SetTemperature(200)
    var printErr = DropItem()
    catch(motErr)
    catch(fanErr)
    catch(tempErr)
    catch(itErr)
}

As appose to catch(), with try() function (example below):

func Dispense() (err error){
    try(SetMotorSpeed(5))
    try(StartFan(5))
    try(SetTemperature(200))
    try(DropItem())
}

I would like to point out that when flexibility is needed err!=nil pattern works better than both catch() and try() proposal IMO. The main goal of the original try() as a function proposal s to remove unnecessary boilerplate when flexibility is NOT needed.

networkimprov commented 5 years ago

If y'all want to manifest something other than try() in 1.14, you need to first prove that try() isn't useful for the great majority of existing code.

The Go team has told us that if we can't prove that, the merits of alternatives are moot.

EDIT: This is a fact. Down-voting facts doesn't make them fiction.

mathieudevos commented 5 years ago

This still would take up a keyword: catch, but only barely improve on the try variant.

The point is to reduce the footgun rating of a solution, not to offer an "improved" novelty.

Many of the arguments again try is that it not improves anything, just dumbifies error handling. This catch can just be written as a package that provides this as a single function. As @networkimprov stated, as long as there's no proof that would improve the try case it's void of use.

What for users that use custom errors that use wrapping functions?

This was covered in comments.

Comments are personal discussions, not part of the proposal, the proposal only has catch with error formatting.

If we plan on taking up a whole keyword, can we at least attach some function to it?

Now I'm not sure if this is a troll post. Most keywords serve exactly one function. return, if, etc.

Return takes multiple values and it's not a function, it has a space after it, clearly marking it as a keyword. Reserved items should be space separated just so it's clear they're not a local function.

All of this can be replaced by having a local package called: errorHandler with 1 public function: Catch. No need to change the language for the sake of changing it (same does technically count for try() proposal).

peterbourgon commented 5 years ago

I support this proposal.

It is important that any change to error handling allows each error in a function to be annotated explicitly and independently from other errors. Returning the naked error, or using the same annotation for every error return in a function, may be useful in some exceptional circumstances, but are not good general practices.

I also support using a keyword instead of a builtin. If we add a new way to exit a function, it is important that it isn't nestable within other expressions. If we have to wait until further improvements to the language allow this to be accomplished without breaking the Go 1 compatibility promise, then we should do that, rather than make the change now.

edit: @networkimprov's #32611 also satisfies my requirements, and I support it as well.

DisposaBoy commented 5 years ago

IMO, this doesn't really improve on if err != nil { ... } fmt on one line.

networkimprov commented 5 years ago

A more flexible one-extra-line proposal is #32611

err = f()
on err, <single_statement>

EDIT: And here is a catch proposal where catch does what you'd expect: #27519

pborman commented 5 years ago

The most common error handling code I have written is the classic:

        ..., err := somefunc1()
        if err != nil {
                return err
        }
        ..., err := somefunc2()
        if err != nil {
                return err
        }

Like @ubikenobi, I think the catch solution (or the on solution just suggested) adds little value:

        ..., err := somefunc1()
        catch(err)
        ..., err := somefunc2()
        catch(err)

while try lets you focus on the code, not the errors:

        ... := try(somfunc1())
        ... := try(somfunc2())

Just as well quickly learned that r is probably an io.Reader and w is probably an io.Writer, we will quickly learn to ignore the try while reading code.

The try proposal really shines when you want to have chaining types and still support errors. Consider:

        v, err := Op1(input)
        if err != nil {
                return err
        }
        v, err = v.Op2()
        if err != nil {
                return err
        }
        result, err := v.Op3()
        if err != nil {
                return err
        }

With try this becomes:

        result := Op1(input).Op2().Op3()

This would actually encourage this paradigm more, which I think would be a good thing.

Catch, on the other hand, make it:

        v, err := Op1(input)
        catch(err)
        v, err = v.Op2()
        catch(err)
        result, err := v.Op3()
        catch(err)

Quite honestly, if the error solution does not support chaining then its value is reduced. If it does not eliminate the extra lines of code between statements, its value is reduced.

One scenario where try is clearly not helpful and the check proposal has a very slight advantage over straight Go is:

        ..., err := somefunc1()
        if err != nil {
                return ..., fmt.Errorf("somefunc1: %v", err)
        }
        ..., err := somefunc2()
        if err != nil {
                return ..., fmt.Errorf("somefunc2: %v", err)
        }

While catch would add some slight value, it certainly does not carry its own weight vs the existing paradigm give it's other issues.

I can contrive a solution that uses try, but it is worse than the original code because each return requires different processing.

As mentioned, the try solution can easily be extended in the future by adding additional parameters, though they need to be readable.

As for the name try, I am not super enamored with it, it will make many people think "where is the catch?". I could suggest must, instead, but right now we associate must with a panic. I don't know a better word choice so try is okay. I certainly don't want something like returnOnError even though it is pretty descriptive!

natefinch commented 5 years ago

@ngrilly mentioned that panic is not a statement, and that's a good point. I'm not entirely sure what the limitations are on what we can enforce on a new builtin. If we can't make catch (or whatever we call it) a statement, making it not return a value is good enough... it still means it'll basically have to be on its own line.

I have updated the proposal to remove the language about being a statement, to avoid confusion.

peterbourgon commented 5 years ago

@pborman

[try] would actually encourage this paradigm more, which I think would be a good thing.

I don't think it's a good idea to encourage returning ~the naked~ an un-annotated error. As a general rule, errors should be individually annotated with context before being yielded up the callstack. Although if err != nil { return err } is the trope often cited in Hacker News comments or whatever, it isn't great practice.

pborman commented 5 years ago

@peterbourgon , the proposal examples already addressed this (fmt.HandleError or something like that). A majority of cases can be handled by a single defer call. Probably should not call this a naked return as return err is not a naked return. return with named return values is a naked return and probably should have never made it into the language. If your error handling becomes to complicated (unique handling per error) then all of these solutions are not ideal and you should use standard Go.

peterbourgon commented 5 years ago

@pborman

Probably should not call this a naked return

You're right, I've edited to call this an un-annotated error.

If your error handling becomes to complicated (unique handling per error)

My claim is that this is not complicated error handling, this is [what should be] the default way to handle errors, and that anything less is shortcuts that shouldn't be encouraged in the general case.

pborman commented 5 years ago

My claim is that this is not complicated error handling, this is [what should be] the default way to handle errors, and that anything less is shortcuts that shouldn't be encouraged in the general case.

I think this is an argument against try, check, handle, throw, must, or any other proposal for simplified error handling.

in truth I don't mind Go's error handling that much. I can see try being beneficial, at least to me. It is the least obtrusive, lightest weight, solution I have seen (it also happens to be an improved version of my second suggestion last fall, which was try foo() rather than try(foo()).

For my own code I might consider adding a function, something like HandleErrorf, that optionally wrapped the returned error with the file, function, and line number of where try was called from. When debugging code I would turn that on so all my errors tell me where they originated (actually, the full call chain). I think some other packages do this already.

natefinch commented 5 years ago

@pborman

if the error solution does not support chaining then its value is reduced.

It seems you and I want different things. I specifically made this proposal because I don't want to encourage people to jam more logic into a single line. I don't want to encourage method chaining. I don't want to hide away error handling. Having to write catch(err) every time your function might exit is good IMO. I want to be very explicit about what can fail and where the function can exit.

ngrilly commented 5 years ago

It is important that any change to error handling allows each error in a function to be annotated explicitly and independently from other errors.

@peterbourgon Most of the time, the error context is the same in the whole function. In such a case, I'd prefer to centralize the annotation in one place, instead of annotating each error independently. Russ commented about this: https://github.com/golang/go/issues/32437#issuecomment-503297387

daved commented 5 years ago

@ubikenobi As far as I understand, your example (where only errors are returned) can be duplicated with this proposal.

func Dispense() (err error){
    catch(SetMotorSpeed(5))
    catch(StartFan(5))
    catch(SetTemperature(200))
    catch(DropItem())
}

The main goal of the original try() as a function proposal s to remove unnecessary boilerplate when flexibility is NOT needed.

And yet it is designed to facilitate flexibility. If what you suggest is true, it's flexibility can/should be reduced to only that which is necessary.

@pborman

... we will quickly learn to ignore the try while reading code.

That's the problem.

Also, function chaining is not something I'm on board to promote. It's only really useful for accumulator style types. If it's, otherwise, some machine being controlled, then the relevant functions don't need to return errors at all since "errors are value" and whatnot. I would use a container type that either provides a final error checking function, or use an error channel with the "body" being called in a goroutine.

ngrilly commented 5 years ago

If the error solution does not support chaining then its value is reduced. If it does not eliminate the extra lines of code between statements, its value is reduced.

I fully agree with this.

pborman commented 5 years ago

@natefinch, I think you should support no changes to the language in that case. It does everything you want already (expect that you still need the if statement, which is even more precise if err != && err != io.EOF ...)

I would support no change to the language over check as I believe check has no real benefits over how Go is today. My feeling is if we are going to add something to Go, the try proposal at least seems like a choice that provides actual benefit at little cost. I actually like it and am not opposed to it going in. If it doesn't go in, I am okay, as long as none of the other proposals I have seen do not go in, either.

peterbourgon commented 5 years ago

@ngrilly

Most of the time, the error context is the same in the whole function.

I don’t agree. Error annotations usually are and should be unique for each received error in a function block.

I also agree with @natefinch that error chaining should not be encouraged.

ngrilly commented 5 years ago

@natefinch About "panic not being a statement": the proposal just needs to mention that catch is a built-in function (instead of being a statement).

ngrilly commented 5 years ago

I don't want to hide away error handling.

@natefinch The try function doesn't "hide" error handling any more than the catch function proposed here.

Having to write catch(err) every time your function might exit is good IMO. I want to be very explicit about what can fail and where the function can exit.

try is not different from catch on this matter: You also have to write try(err) every time the function might exit. It's also "very explicit about what can fail and where the function can exit".

daved commented 5 years ago

@ngrilly

The try function doesn't "hide" error handling any more than the catch function proposed here.

It's position in code differs, it's flexibility differs. The difference results in noise. The noise results in it being ignored. try, as it is, will be something to locate within it's own noise. This proposal reduces the broadness of the noise down to a clear tone that is easy to parse and hard to ignore.

ngrilly commented 5 years ago

@peterbourgon

I don’t agree. Error annotations usually are and should be unique for each received error in a function block.

Have you read the comment from Russ I linked previously?

Russ agrees with you when you write that "error annotations should be unique for each received error in a function block". But he argues that it is the callee responsibility (not the caller responsibility) to decorate the error with the context. For example, the error returned by os.Open is "open /etc/passwd: permission denied," not just "permission denied."

I also agree with @natefinch that error chaining should not be encouraged.

Why? This is one of the main use case for similar constructs in Rust, Swift, Haskell, etc. People keep repeating try will make our code unreadable, but Rust has introduced the try! macro and then the ? operator a long time ago now, and they haven't suffered from this hypothetical problem. I fail to see why it would have worked for Rust, and it wouldn't work for us.

daved commented 5 years ago

People keep repeating try will make our code unreadable, but Rust ...

All due respect to Rust, please don't use it as an example of readability. For it's own goals, it is clearly a great language. For the benefits that Go provides and I am drawn to, there is no relation whatsoever.

ngrilly commented 5 years ago

@daved

It's position in code differs, it's flexibility differs. The difference results in noise. The noise results in it being ignored. try, as it is, will be something to locate within it's own noise. This proposal reduces the broadness of the noise down to a clear tone that is easy to parse and hard to ignore.

It's mostly an aesthetic argument. It's in the eye of the beholder.

@ubikenobi has shared earlier an example in which try seems more readable to me than catch:

With catch:

func Foo() (err error) {
    file1, err1 := open("file1.txt")
    catch(err1)
    defer file1.Close()
    file2, err2 := open("file2.txt")
    catch(err2)
    defer file1.Close()
}

With try:

func Foo() (err error) {
    file1 := try(open("file1"))
    defer file1.Close()
    file2 := try(open("file1"))
    defer file2.Close()
}

Edited: used := instead of var = (thanks @daved)