golang / go

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

proposal: leave "if err != nil" alone? #32825

Closed miekg closed 5 years ago

miekg commented 5 years ago

The Go2 proposal #32437 adds new syntax to the language to make the if err != nil { return ... } boilerplate less cumbersome.

There are various alternative proposals: #32804 and #32811 as the original one is not universally loved.

To throw another alternative in the mix: Why not keep it as is?

I've come to like the explicit nature of the if err != nil construct and as such I don't understand why we need new syntax for this. Is it really that bad?

ghost commented 5 years ago

I think it might be a good idea at this point to write a website to keep data for tryhard on different packages and visualize them. Maybe modify a little golang/gddo (godoc.org) can do the job.

kidlj commented 5 years ago

I prefer leaving if err != nil alone. But if we have to add something for error handling, here is a new proposal that add throws keyword for it.

32852

jboursiquot commented 5 years ago

Without repeating some of the arguments already laid out here, I echo the sentiment to leave if err != nil as is.

The perspective I can offer is this: As someone who has taught Go to hundreds of newcomers (both to programming and to Go from other languages), if err != nil has never been a problem for them. The experienced programmers in my workshops find it unusual at first but quickly learn to love the explicit nature of error handling in Go.

There are larger concerns we can address in the language and the clear reaction of the community to this issue says if err != nil isn’t one of them.

Kundeshi commented 5 years ago

Go is perfect for so many reasons. Chief among them is “if err != nil”. It might seem verbose but for people learning to code, it makes it easier to debug your code and correct code.

llorllale commented 5 years ago

@davecheney

I'm not going to take a position on that. However, what I am finding is their little supporting evidence to suggest that

a. there are a lot of locations where try is applicable to existing go codebases b. error handling in general constitutes a significant portion of SLOC, based on my own measurements and the numbers mooted by the Go team, ref https://youtu.be/RIvL2ONhFBI?t=440 timecode 07:26

I'm afraid that in the current climate any examples we find would just be dismissed as "well that's probably not good code".

Here's an example:

llorllale:~/go/src/github.com/hyperledger/fabric$ cloc --exclude-dir=vendor .
    2406 text files.
    2256 unique files.                                          
    3130 files ignored.

http://cloc.sourceforge.net v 1.60  T=6.69 s (272.8 files/s, 58350.9 lines/s)
--------------------------------------------------------------------------------
Language                      files          blank        comment           code
--------------------------------------------------------------------------------
Go                             1751          54365          34149         294005
YAML                             35            547           2171           2060
Bourne Shell                     26            354            325           1312
make                              3            135             96            418
CSS                               1             40             14            140
HTML                              3              7              5             63
Python                            1             50            103             57
Bourne Again Shell                1              1              6             50
Java                              3              7              4             26
XML                               2              1              4              2
--------------------------------------------------------------------------------
SUM:                           1826          55507          36877         298133
--------------------------------------------------------------------------------
llorllale:~/go/src/github.com/hyperledger/fabric$ tryhard -l . | grep -v vendor | less | wc -l
1417
robfig commented 5 years ago

In fairness, the data about number of locations that tryhard found can be confused by conventions which require wrapping errors. For example, if your company convention is to

if err != nil {
   return errors.Wrap(err) 
} 

...
if err != nil {
   return errgo.Notef(err, "error doing x") 
} 

that would not be reported by tryhard.

We have such a convention at my company. Doing a simple search-and-replace to revert those to the naked error returns gives me these results:

---------------------------------------------------------------------------------------
Language                             files          blank        comment           code
---------------------------------------------------------------------------------------
Go                                    2488          40317          15901         297038

tryhard reports 2736 replacements, but doing a manual review of remaining wrapping looks like that undercounts by about 1850, so I'd estimate a total of ~4500 try usages in our codebase of 300k lines.

(Personally, I am in favor of the current explicitness of error handling and don't mind it.)

lpar commented 5 years ago

For example, if your company convention is to [wrap errors with a custom message] that would not be reported by tryhard.

That's the point — the try proposal only simplifies if err != nil return err naked returns, it doesn't support wrapping errors with a custom message and context.

nkcmr commented 5 years ago

The only repetitiveness of if err != nil I believe could be fixed having to also specify the zero values of the other return values. The language could be updated to elide that. For example:

In today's Go, if I have a function with this signature:

func add(x, y string) (int, error)

Somewhere in the function, I would have to write:

func add(x, y string) (int, error) {
    // ...
    if err != nil {
        return 0, err
    }

Forcing the writer to repeat the same zero values throughout the function.

It would be much easier (and with little cost to error verbosity and readability) if the language could automatically fill in the zero values for the non-error return values:

func add(x, y string) (int, error) {
    // ...
    if err != nil {
        return ..., err
    }
    // ...
}
func main() {
    add("8", "beep") // returns 0, error(`strconv.ParseInt: parsing "beep": invalid syntax`)
}

I can say from experience with a lot of code that interacts with DB queries and calls, having to repeat the zero values across the functions is the only negative of Go-style error handling. Otherwise I agree with the sentiment of this proposal: Leave if err != nil alone!

Note: yes, named return values can sort of accomplish this (https://play.golang.org/p/MLV8Y52HUBY), but after implementing a few functions in my own codebases using this technique, I was reminded how much of a foot-gun named return values are; I always end up shadowing the named return value.

robfig commented 5 years ago

For example, if your company convention is to [wrap errors with a custom message] that would not be reported by tryhard.

That's the point — the try proposal only simplifies if err != nil return err naked returns, it doesn't support wrapping errors with a custom message and context.

That's true, I was thinking of the variant that allowed adding a descriptive string. The vast majority (~4000 / 4500) of our error returns are a no-context errgo.Mask(err), which I was considering equivalent to a no-description try(), but currently it would be a reduction in functionality since errgo adds stack information and try does not (yet).

theckman commented 5 years ago

@ianlancetaylor there is a proposal here. @miekg is proposing that you as one of the leaders of our language no longer pursue the replacement of if err != nil with some other construct that contradicts the spirit of error handling as decided upon by the original Go Authors. To me personally, it feels like you're trying to assert the unimportance of this ask by moving it to golang-nuts instead of treating it like our other proposals. That may not be your intent, but it's the impact I feel.

Our method of error handling is unique, and I believe it to be a massive value add over other languages. It completely changed how I think about errors in the systems I build, and as a result I became a stronger software engineer. I don't want us to pander to the loud minority, or outsiders, in the interest of getting more Go developers. I think we should take hard lines on certain things, with the way we choose to handle errors being one of them because it ultimate makes us better by trading off code brevity.

This is an opportunity for the team inside Google to build more trust and faith with the community, or to continue the trajectory we are currently on which is not good for the language, ecosystem, or its users.

I ask that the Go Team accept this proposal as-is, while continuing to pursue other unrelated language iterations that are a clearer value add.

as commented 5 years ago

The tracker may not have threading, but I would personally much rather have the guarantee that this proposal is responded to in an official capacity and not relegated to the Google group where it can fade quietly into obscurity.

The topic has already been discussed on the Google group too.

chtisgit commented 5 years ago

The current version of #32437 is unsatisfying. The try() builtin hides a lot of execution paths to the untrained eye. The original proposal with check and handle was very understandable and the check keyword stood out.

Now, the try() builtin looks like a function - it is not obvious that it can change control flow. We also have panic(), but it is (I believe) always on a line of its own, has a prominent name and its use is scarce. try() on the other hand, could hide inside a complex expression.

ngrilly commented 5 years ago

@theckman Robert has designed the first iterations of Go with Rob and Ken, and Robert and Russ have joined the team early. They have been working on Go since the beginning. I think we can trust them to know if a proposal "contradicts the spirit of error handling as decided upon by the original Go Authors".

I dislike the principle of a proposal that would freeze error handling as it is today. Such a proposal would forbid all future proposals on this topic.

Why not just accept to iterate the design instead? We had the check/handle proposal. But some drawbacks were discussed. This led to the try proposal. Some drawbacks of this proposal are discussed now. Maybe this will lead to another, better proposal, until the right approach is found.

Our method of error handling is unique

The error handling in Rust is conceptually similar to what we do in Go (errors are values, explicit control flow, except we use multiple return values when they use sum types instead). Rust had the same issue as Go with verbose error handling. This led Rust to add the try! macro, and eventually the ? operator. I would say the Rust community is even stricter than the Go community regarding error handling (the error handling RFCs and discussions are enlightening). They have found a way to decrease error handling verbosity without the slippery slope of bad error handling. I'm sure we can too.

the trajectory we are currently on which is not good for the language, ecosystem, or its users

What are you talking about? Go is constantly improving. It's amazing to have access to such a great language, tooling and documentation for free (as in free speech).

sirkon commented 5 years ago

@theckman Robert has designed the first iterations of Go with Rob and Ken, and Robert and Russ have joined the team early. They have been working on Go since the beginning. I think we can trust them to know if a proposal "contradicts the spirit of error handling as decided upon by the original Go Authors".

I dislike the principle of a proposal that would freeze error handling as it is today. Such a proposal would forbid all future proposals on this topic.

Why not just accept to iterate the design instead? We had the check/handle proposal. But some drawbacks were discussed. This led to the try proposal. Some drawbacks of this proposal are discussed now. Maybe this will lead to another, better proposal, until the right approach is found.

Our method of error handling is unique

The error handling in Rust is conceptually similar to what we do in Go (errors are values, explicit control flow, except we use multiple return values when they use sum types instead). Rust had the same issue as Go with verbose error handling. This led Rust to add the try! macro, and eventually the ? operator. I would say the Rust community is even stricter than the Go community regarding error handling (the error handling RFCs and discussions are enlightening). They have found a way to decrease error handling verbosity without the slippery slope of bad error handling. I'm sure we can too.

the trajectory we are currently on which is not good for the language, ecosystem, or its users

What are you talking about? Go is constantly improving. It's amazing to have access to such a great language, tooling and documentation for free (as in free speech).

The history of Rust’s development shows the dudes behind it didn’t have an idea what they are doing. They basically copied error processing principles from Haskell, but these are not a good match for imperative (real world?) programming. Their ? macro is just a workaround for initially failed error processing system.

sirkon commented 5 years ago

@ianlancetaylor there is a proposal here. @miekg is proposing that you as one of the leaders of our language no longer pursue the replacement of if err != nil with some other construct that contradicts the spirit of error handling as decided upon by the original Go Authors. To me personally, it feels like you're trying to assert the unimportance of this ask by moving it to golang-nuts instead of treating it like our other proposals. That may not be your intent, but it's the impact I feel.

Our method of error handling is unique, and I believe it to be a massive value add over other languages. It completely changed how I think about errors in the systems I build, and as a result I became a stronger software engineer. I don't want us to pander to the loud minority, or outsiders, in the interest of getting more Go developers. I think we should take hard lines on certain things, with the way we choose to handle errors being one of them because it ultimate makes us better by trading off code brevity.

This is an opportunity for the team inside Google to build more trust and faith with the community, or to continue the trajectory we are currently on which is not good for the language, ecosystem, or its users.

I ask that the Go Team accept this proposal as-is, while continuing to pursue other unrelated language iterations that are a clearer value add.

They can’t do anything serious with current type system from 60s. They need to finally borrow 80s ideas in their Go 2.0

theckman commented 5 years ago

What are you talking about? Go is constantly improving. It's amazing to have access to such a great language, tooling and documentation for free (as in free speech).

@ngrilly that last part is probably for a larger discussion. Without derailing this proposal, but putting some closure to that comment, there is a growing sentiment of misalignment between users and leadership in the community/ecosystem.

For the rest of the discussion, I don't think adding more cognitive overhead to the syntax is a win. I'm glad they found something that worked for them, we aren't them.

andreynering commented 5 years ago

I just opened a proposal for the inline if statement: https://github.com/golang/go/issues/32860

Reference: https://github.com/golang/go/issues/32825#issuecomment-506707539

av86743 commented 5 years ago

How much nicer this world would become if everyone submitting their proposal about whatever new golang 2.0 feature they would just so love to have, would also provide a branch of their fork of https://github.com/golang/go (and whatever other repositories necessary) which implements that proposal.

Don't you agree?

theckman commented 5 years ago

@av86743 Seems beyond the scope of this proposal. Please file a Proposal suggesting that course of action.

I do see some challenge with that, like the risk of a lot of wasted effort before someone turns it down based on something in the proposal document itself. Then you spent all that time on a fork that won't even be reviewed.

codenoid commented 5 years ago

how about this syntax :

# call error handler
try callFunction(), errorHandler()

# error handler with anonymous function
variable := try callSomething(), func(err *Error) { # error handling }
ianlancetaylor commented 5 years ago

@theckman I apologize if it seems like my suggestion of moving this discussion elsewhere makes it seem that it is unimportant. I explained my reasons in my request, and I believe they still stand. The Go team considers mailing list discussions as well as proposals.

Since you mention "the original Go Authors" I think it's worth pointing out that the "try" proposal was made by @griesemer who is one of the three original Go authors.

win-t commented 5 years ago

I strongly agree with this proposal, I think the only thing needs to be changed is just go fmt, make go fmt to allow one line if statement.

I really want one line of

if err != nil { return wrappedErr{err} }

instead of three line of

if err != nil {
    return wrappedErr{err}
}
av86743 commented 5 years ago

@av86743 Seems beyond the scope of this proposal. Please file a Proposal suggesting that course of action.

@theckman You are telling me what to do, and this is not just not polite, it is outward rude. You can attempt to position yourself whatever way you choose, however neither myself nor, presumably, anyone else here is your "go fetch" monkey to jump when you say so.

I do see some challenge with that, like the risk of a lot of wasted effort before someone turns it down based on something in the proposal document itself. Then you spent all that time on a fork that won't even be reviewed.

It would only be a "wasted effort" for [... description in an entirely appropriate language omitted for the sake of brevity ...].

For a coder, though, that would be a trivial, but useful exercise and, at the same time, a service to Go community.

theckman commented 5 years ago

@av86743 I think what you suggested is an interesting idea and I didn't want it to get lost as a comment in an unrelates issue. If you've no interest in pursuing it in an official capacity for consideration, I apologize for advocating that you raise a separate issue.

bitfield commented 5 years ago

Even though this specific proposal comes from @griesemer, I find it hard to believe that he's been simmering with inner rage for ten years about the verbosity of unwrapped error returns in Go. He is, however, an excellent engineer, and engineers come up with solutions to (perceived) problems; it's very hard to stop them. We like solving problems. The mere sniff of a problem is enough for us to start coming up with all sorts of ideas. Once that thought process is fairly well advanced, it's hard for any of us to let go of our putative solutions emotionally and consider that maybe it isn't really a problem after all. After all, we've had a baby, intellectually speaking, and it's not easy to just abandon it.

For what it's worth, my private suspicion is that the Go team's reasoning process on this has gone something like:

  1. Go is hugely popular and widely used, so thousands of people have, naturally, comments, suggestions, and complaints about it.
  2. You can only stonewall for so long. The Go team feels under enormous pressure to do something about all the noise from the community.
  3. This is something.
godcong commented 5 years ago

how about add a catch() function in defer to catch if the try was found some error like recover(). example:

func doSomthing()(err error){
    //return error
}

func main(){
    defer func(){
        if err:=catch();err!=nil{
            //found error
        }
    }()

    try doSomthing()
}

on many functions

func Foo() (err error) {
    defer func(){
        if err:=catch();err!=nil{
            //found error
        }
    }()

   try{
    file1 := open("file1")
    defer file1.Close()
    file2 := open("file2")
    defer file2.Close()
   }
   //without try
    file3,err := open("file3")
    defer file3.Close()
 }
sorenvonsarvort commented 5 years ago

how about add a catch() function in defer to catch if the try was found some error like recover(). example:

func doSomthing()(err error){
    //return error
}

func main(){
    defer func(){
        if err:=catch();err!=nil{
            //found error
        }
    }()

    try doSomthing()
}

on many functions

func Foo() (err error) {
    defer func(){
        if err:=catch();err!=nil{
            //found error
        }
    }()

   try{
  file1 := open("file1")
  defer file1.Close()
  file2 := open("file2")
  defer file2.Close()
   }
   //without try
    file3,err := open("file3")
    defer file3.Close()
 }

How does this helps to handle each error in a separate way?

griesemer commented 5 years ago

Some clarifications:

  1. The try proposal does neither introduce new syntax nor a new keyword, contrary to what some people have been claiming on this thread. It merely introduces a new built-in, about the most minimal change one might be able to do to add new functionality. Please be precise when discussing this, because it matters. There's a huge difference between adding new syntax and a new keyword, and a built-in. The former is a major change, that latter a relatively minor addition. What the try proposal suggests is a relatively minor addition.

  2. I agree with @ianlancetaylor that this discussion is better held elsewhere (golang-nuts). There is no proposal here.

  3. Indeed, @bitfield, I have zero "inner rage about about the verbosity of unwrapped error returns in Go", thank you :-) But I do think that error checking is more verbose that perhaps necessary; and the fact that this same sentiment has been brought up by the community repeatedly is a clear indicator that we (the Go team) are not alone with this belief. I wouldn't go as far as saying there's a lot of pressure to do "something" - we've been working on this on and off for a long time now, and we're quite content to wait for the "right" approach.

The try proposal is about the most minimal solution we have found (with significant help from community contributions) that addresses the error checking problem. The try proposal is very explicit about the fact that it won't help at all if every error test requires handling the error in some specific way. try only helps when all errors in a function are tested and handled the same way (and then we recommend using defer), or when they are simply returned. It's hard to be more explicit here, but let's repeat what the proposal states: try won't help in all error scenarios. It helps a in significant number of cases. For everything else, use if statements.

sirkon commented 5 years ago

@griesemer try is too error prone: there’s no RAII in Go, so we can’t just leave the function in many cases.

dsnet commented 5 years ago

@sirkon, I'm not sure how RAII is relevant to this discussion. try replaces existing patterns of if ..., err := f(); err != nil { return ..., err } with a ... := try(f()). If there was a resource freeing bug by using try, then it certainly existed beforehand as well. The introduction of try neither enhances nor prevents the resource freeing bug.

sirkon commented 5 years ago

@sirkon, I'm not sure how RAII is relevant to this discussion. try replaces existing patterns of if ..., err := f(); err != nil { return ..., err } with a ... := try(f()). If there was a resource freeing bug by using try, then it certainly existed beforehand as well. The introduction of try neither enhances nor prevents the resource freeing bug.

Read the thread, there was an example:

info := try(try(os.Open(fileName)).Stat())
ianlancetaylor commented 5 years ago

@sirkon I've seen that example several times now. I agree that it is interesting. But let's think about it a bit more. The proposed try builtin is basically syntactic sugar for a certain kind of boilerplate found in Go code. So we can convert that example to the original code.

    f, err := os.Open(fileName)
    if err != nil {
        return err
    }
    info, err := f.Stat()
    if err != nil {
        return err
    }

That code has the same bug. I've certainly seen code like that. It's not obvious to me that the try builtin makes that bug easier to write or harder to see.

griesemer commented 5 years ago

[Looks like @ianlancetaylor just beat me to it.]

@sirkon This bug is already possible, try or not - Go doesn't prevent you from writing bad code. Or turning this around, using bad code as a reason why try shouldn't be permitted is not a convincing argument. Instead, go vet should flag problematic cases.

defer is the Go idiom to clean up when a function returns, and that works well. The correct approach here of course would be:

f := try(os.Open(filename))
defer f.Close()
info := try(f.Stat())

compare this to:

f, err := os.Open(filename)
if err != nil {
   return ..., err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
   return ..., err
}

Using try the source concentrates on the primary concern: getting a file's file info. Using the traditional approach, most of the source code is concerned about possible errors; and it's all the same. Even if we want to decorate the error, the try approach works beautifully:

defer errd.Wrap(&err, "failed to do X for %s", filename)
f := try(os.Open(filename))
defer f.Close()
info := try(f.Stat())

using something like an errd package (see issue #32676).

daved commented 5 years ago

@griesemer My future self doing code review still keeps screaming that the control flow mechanism should be on it's own line. Can this approach be valid (no enforcement) within the current proposal? Besides readability, refactoring to more detailed error handling logic is eased.

defer errd.Wrap(&err, "failed to do X for %s", filename)
f, err:= os.Open(filename)
try(err) // check is so much a better term
defer f.Close()
info, err := f.Stat()
try(err)

Also, this approach to handle looks great here, but won't multiple defers make a mess? Or will there be a function-scoped sync.Once sort of thing (I didn't see clarification in the errd issue)? If so, would anonymous funcs be granted their own scope? And shadowing... eesh - who's on first, what's on second?

This all feels like there will end up being two "modes" of writing Go code. Say it isn't so.

cstockton commented 5 years ago

@griesemer While you're right the bug is possible today as well, I strongly feel it will become more prevalent in the future with the current implementation of try. Someone coming from just about any popular language I can().think.Of(Has)Chaining().Of(Methods) ingrained into them for better or worst. These languages all provide their own idioms that make the patterns natural and safe. They hit an immediate road block with Go as the type system forces them to assign every value with an accompanying failure condition, there is simply no reasonable pattern to avoid this (or the try proposal would not exist).

If this proposal is accepted they will a way to avoid the if err boiler plate, allowing them to write code in a way familiar to them. Except they will have nearly a decade of Go code in stackoverflow answers, blog posts etc that was written before try was created. They will quickly learn to simply drop the err and if statement with try. They want a file size they can paste code wrapped in try until they can access the field the want just like the Stat() in the try proposal. It's a pattern they are use to so it's only natural they apply it while writing Go. Given the most targeted Go OS treats everything as a file it's fair to assume more resource leaks will occur.

Which brings me to why I strongly disagree with the statement "you can already do this today" - because you simply can't. Sure - you can leak a file handle. But Go does not give a programmer the opportunity to skip having the identifier in scope and thus also leaking the file. Each identifier f skipped is a file handle leaked. Usage of the feature requires that certain prominent idioms in the Go ecosystem are broken. Thus- introducing the feature as designed today demonstrably increases the risk of leaking resources in Go.

That said as I mentioned in https://github.com/golang/go/issues/32825#issuecomment-506882164 I actually would support try if a couple small adjustment were made, I think the change would be a welcomed addition to the language. All I think try needs is to make it only valid on the RHS of an assignment and not allow the return value to be addressable. Make the "good" examples of try usage (tend to be one try per line) be the "only" way to use try, i.e.:

info := try(try(os.Open(filename)).Stat()) // compiler error
f := try(os.Open(filename)) // valid
// we were forced to assign f, so we still have an identifier to Close (serve linters and users alike)
defer f.Close()
info := try(f.Stat())
a, b := try(strconv.Atoi("1")), try(strconv.Atoi("2")) // also valid 
a, b := try(strconv.Atoi("1"), strconv.Atoi("2")) // maybe?
a, b := try strconv.Atoi("1"), strconv.Atoi("2")

I think this will fit naturally better in the language, maintain all the current benefits of try (other than nesting them, if yo consider that a benefit) without any of the drawbacks. I don't think nesting of try does anyone any favors, it saves very little, but provides unlimited possibilities for abuse. I'm not feeling particularly evil today, so this is the best I can do:

total := try(try(os.Open(filename)).Stat()).Size() + try(strconv.Atoi(try(ioutil.ReadAll(os.Stdin))))

But we will think of worst, if you let us.

griesemer commented 5 years ago

@daved Putting try(err) on a second line is fully supported with the existing try proposal: try simply wants an argument that evaluates to one or more values where the last value is of type error, which is naturally satisfied when you write try(err).

I'm not sure I follow your concern regarding defer - if there are different handlers required, defer might not be the right choice; instead the traditional if may be needed (as spelled out in the proposal).

griesemer commented 5 years ago

@cstockton I agree that nested try's can be very problematic; but I also believe if we had try, most of the code would look like the (valid) examples you have given. Professionals don't code in a vacuum, they tend to follow style guides, code reviews, and good practices. So I am just not that concerned here (well knowing that if it's possible to write bad or incorrect code, somebody will do it - so be it).

As a matter of style, we have not put restrictions such as the one you're favoring into the language - we have used go vet for that. In the end, for the written software the effect is the same. But by not having it in the language, we are not tying ourselves down. It's tricky to get those restrictions just right, and they make the spec unnecessarily complicated. It's simply much easier to adjust go vet and make it smarter as we learn more than adjusting the language.

daved commented 5 years ago

@griesemer Thanks for the clarification. In the code example, if the first line was var err error, would the wrap potentially affect both checked errors? I've seen talk about shadowing being a concern that may be dealt with in the future. How does/might that relate to this?

godcong commented 5 years ago

how about add a catch() function in defer to catch if the try was found some error like recover(). example:

func doSomthing()(err error){
    //return error
}

func main(){
    defer func(){
        if err:=catch();err!=nil{
            //found error
        }
    }()

    try doSomthing()
}

on many functions

func Foo() (err error) {
    defer func(){
        if err:=catch();err!=nil{
            //found error
        }
    }()

   try{
    file1 := open("file1")
    defer file1.Close()
    file2 := open("file2")
    defer file2.Close()
   }
   //without try
    file3,err := open("file3")
    defer file3.Close()
 }

How does this helps to handle each error in a separate way?

like other users committed

func Foo() (err error) {
    defer func(){
        if err:=catch();err!=nil{
            //found error
        }
    }()

    file1 :=try open("file1")
    defer file1.Close()
    file2 :=try open("file2")
    defer file2.Close()

        //without try
       file3,err := open("file3")
       defer file3.Close()
 }
griesemer commented 5 years ago

@daved In these examples, the assumption was that err is the name of the result error. try will always set that result error variable, no matter the name (or absence of a name). If you have a local variable called err then that's a different variable. If you want to refer to the result error, it will have to have a different name. Note that it's already the case that this is not valid:

func f(...) (..., err error) {
   var err error // << err already declared
   ...

On the other hand, if you write:

func f(...) (..., err error) {
   a, b, ... err := g() // redeclaration of err
   ...

the err in the assignment is simply the same one named in the result parameter list. There's nothing different here than what was already the case for a very long time.

PS: We should probably stop hijacking this issue for try discussions and move back to the original proposal - it will be unlocked and open for discussion tomorrow (July 1) again.

griesemer commented 5 years ago

@godcong A catch() function (or similar) will only allow you to get the error, not to set it (and usually one wants to set the error of the enclosing function in a deferred function operating as an error handler). It could be made to work by making catch() return a *error which is the address of the enclosing function's error return value. But why not just use the error result name instead of adding a new mechanism to the language? See also the try proposal where this is discussed.

Also, see the PS above.

beoran commented 5 years ago

@griesemer

It's hard to be more explicit here, but let's repeat what the proposal states: try won't help in all error scenarios. It helps a in significant number of cases. For everything else, use if statements.

I think this is exactly the fatal flaw of the try() proposal: where before there was one and only one way to do error checking, now there will be two, intermingled all through the code base. Also, at least for the code base I am working on, less than 20% of if err != nil can be replaced with try(), which while not insignificant, does not seem like worth while enough to create a split in error handling styles.

Personally I would have preferred an error handling construct that is powerful enough to replace 95% of all if err != nil cases in stead. I think that is what many people would have liked as well.

cstockton commented 5 years ago

@griesemer I agree that people will learn and tooling will be a must since the style guides, good practices, examples, documentation and so on you are referring to will all be outdated. I think it's clear that try as currently proposed introduces subtle new ways to write incorrect software. What is not clear is how dismissing this fact under the premise that people can always write incorrect software is a valid counter argument?

I'll switch angles here, what is the use case for nesting try statements that is strong enough for the potential side effects I've outlined? How does Go code today gain benefit by allowing a chain-able nest-able try-separated parenthesis-party to wildly appear anywhere? My guess is that it doesn't and I don't think anyone asked for try nesting, it came with the proposal because it's implemented as a function. You don't want to add any constraints like removing nesting / being addressable to limit nesting abuse or subtle errors because it would make the language spec more complex. Is the theme here to prevent introducing complexity to the language, or to add a better way to handle errors?

Because if the goal here is to not make the language spec more complex, the choice is clear: Do not add a new function with generic returns & params, is arbitrarily nestable, provides control flow and changes the arity of the values it is given (but only if they satisfy a specific builtin interface) and probably more that I am forgetting e.g. a function with unprecedented complexity. If the goal is to improve error handling I think it should have to do that without introducing new ways to produce errors.

sirkon commented 5 years ago

@sirkon I've seen that example several times now. I agree that it is interesting. But let's think about it a bit more. The proposed try builtin is basically syntactic sugar for a certain kind of boilerplate found in Go code. So we can convert that example to the original code.

    f, err := os.Open(fileName)
    if err != nil {
        return err
    }
    info, err := f.Stat()
    if err != nil {
        return err
    }

That code has the same bug. I've certainly seen code like that. It's not obvious to me that the try builtin makes that bug easier to write or harder to see.

It is obvious for me "slower" traditional flow leaves more room to notice file should be closed and this try provokes these sort of leaks as people tend to prefer shortcuts over long way.

sirkon commented 5 years ago

@godcong A catch() function (or similar) will only allow you to get the error, not to set it (and usually one wants to set the error of the enclosing function in a deferred function operating as an error handler). It could be made to work by making catch() return a *error which is the address of the enclosing function's error return value. But why not just use the error result name instead of adding a new mechanism to the language? See also the try proposal where this is discussed.

Also, see the PS above.

Go's type system stucked in 60s and thus naturally unable to handle edge cases well. If you were farsighted enough to borrow 80s ideas you would have methods to control subtle error-prone flows. You are trying to construct glass and metal building in a medieval village now: the bad thing these medieval villages don't have electricity and water pipes thus you will not have it too.

av86743 commented 5 years ago

It will be interesting to see to what extent whatever new-and-improved err facilities will be employed in golang/go itself. If at all.

It will be also interesting to see whether go2 fmt will have an option to output plain go1.x.

rockmenjack commented 5 years ago

From my own experience, ever since I added context in my returned error by:

import "github.com/pkg/errors"
func caller(arg string) error {
  val, err := callee(arg)
  if err != nil {
    return errors.Warpf(err, "failed to do something with %s", arg)
  }

  err = anotherCallee(val)
  if err != nil {
    return errors.Warpf(err, "failed to do something with %s", val)
  }

  return nil
}

the support team rarely need my input for issues arises in production.

IMHO, I believe improving error handing is not about reducing boilerplate code but provide a more convenient way to add context to errors. I still can not find a good sensible way to use try().

Maybe add context in defer:

func caller(arg string) (err error) {
  defer func() {
    switch t := err.(type) {
      case CalleeErr:
        err = errors.Wrapf(err, "failed to do something with %s", arg)
      case AnotherCalleeErr:
        err = errors.Wrapf(err, "failed to do something with %s", val)
    }
  }()

  val := try(callee(arg))
  try(anotherCallee(val)
  return nil
}

Doesn't seem to save a lot of typing, but we sacrifice readability and performance.

Might end up using try() in this way:

func caller(arg string) error {
    val, err := callee(arg)
    try(errors.Warpf(err, "failed to do something with %s", arg))

    err = anotherCallee(val)
    try(errors.Warpf(err, "failed to do something with %s", val))

    return nil
  }

It does reduce a few lines, that's it.

turtleDev commented 5 years ago

to me, most solutions to this problem seem to break the one thing that I thought separated go from other languages that use exceptions: the error handling mechanism is not used as flow of control. Most of these solutions add some form of control flow (check/handle, try, catch, expect) at which point I think that the Go team might as well add exceptions and call it a day.

Though I'd love it if we could have a special case of if and return that kinda resembles ruby

return err if err != nil

P.S. Pardon my inexperience in Language design, if I've said something stupid, please feel free to point it out and educate me.

sorenvonsarvort commented 5 years ago

how about add a catch() function in defer to catch if the try was found some error like recover(). example:

func doSomthing()(err error){
    //return error
}

func main(){
    defer func(){
        if err:=catch();err!=nil{
            //found error
        }
    }()

    try doSomthing()
}

on many functions

func Foo() (err error) {
    defer func(){
        if err:=catch();err!=nil{
            //found error
        }
    }()

   try{
  file1 := open("file1")
  defer file1.Close()
  file2 := open("file2")
  defer file2.Close()
   }
   //without try
    file3,err := open("file3")
    defer file3.Close()
 }

How does this helps to handle each error in a separate way?

like other users committed

func Foo() (err error) {
    defer func(){
        if err:=catch();err!=nil{
            //found error
        }
    }()

  file1 :=try open("file1")
  defer file1.Close()
  file2 :=try open("file2")
  defer file2.Close()

        //without try
       file3,err := open("file3")
       defer file3.Close()
 }

In Your example You handle all the errors by the same deffer. What happens if You want to add custom message and custom information to the error?

cstockton commented 5 years ago

@ianlancetaylor Has anyone suggested augmenting the ":=" operator to support "inferred returns" - Basically behave exactly as try without a function call. This would solve a lot of concerns I've seen on both sides:

I think if we were to simply infer returns like we do types, things look concise and feel more "Go" like:

f, err := os.Open(filename)
if err != nil {
   return ..., err
}
defer f.Close()

info, err := f.Stat()
if err != nil {
   return ..., err
}

Insert correct computer science description for this behavior here, until then settle for inferred returns via Short variable declarations:

f := os.Open(filename)
defer f.Close()
info := f.Stat()

Which looks much more tidy than:

f := try(os.Open(filename))
defer f.Close()
info := try(f.Stat())

This solves all of the concerns I listed above while (in my opinion) feeling a bit more "Go like" and maintaining backwards compatibility. It seems a bit easier to explain as well, the "implicit" return on the function call for try() feels really out of place given the ubiquitous meaning of try in every other language. I can't speak 100% for implementation but it seems like it could potentially be done with roughly the same effort? The AssignStmt could have a field added which specifies which expressions on the LHS omitted their err values to inform the same backend compilation as a builtin?