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?
I second this. I really like how decorating every error before returning it adds human readable documentation to the source (usually we format our errors as "could not [what I am doing in these lines of code]: [previous error]") and also to the users reading errors.
Errors generated this way are extremely informative and much easier to read than stack traces. Printed errors that include stack traces usually assume you have ready access to sources (administrators might not have such access) and actually know your way in the code.
Errors without any form of context or tracing (the bare string "EOF") are absolutely useless. I think having shortcuts that make it easier to return naked errors will make Go programs print a lot of useless errors.
If anything, we should push and support decorating errors with context, maybe with new vet and lint rules.
I also like the explicitly error check. try
is confusing and the implicit return is strange.
I think that instead of rethinking errors, we could try an alternative approach to make these checks shorter.
Here's an example which I don't necessarily agree:
value, err := foo()
return err if err != nil
This would allow an shorter but still explicit approach. And it'd allow adding context!
That said, inline ifs are a Ruby thing and don't feel very Goish, but this is just brainstorming. Maybe we find something else.
EDIT: I added a proposal for this here: https://github.com/golang/go/issues/32860
there should be only one way of doing a thing
[...]Why not keep it as is?
I think it's fair to say that we all know the answer to this. You need only go read one of the various proposals to find out the answer if you sincerely don't know.
IMO, there's too little detail here for us to have a focused discussion (i.e. I don't think it qualifies as a proposal) and it will soon turn into another bike-shed full of circle-jerking and ideas that make the code less readable.
So much this.
Arguably I got into Go because of this explicit error handling. It sits somewhere between implicit try-catch that many languages go for and function types like Option or Maybe, which favors being returned to the user and be handled explicitly.
I'm not sure if a new construct would really solve this. If you wrapped if err := nil
in a helper function like this, it might help a little (pardon my rusty Go):
func handleErr(err error, cb func(error)) {
if err := nil {
cb(err)
}
}
But the issue that makes this helper function less generally useful is the type system, which is a different topic.
I second this. if err != nil { return err }
is not part of any code in our code base. Therefore the try "macro" does not make any sense at all. We only return wrapped errors, with a message describing the context.
Adding context via defer does not make sense either, since we want to return different error messages to distinguish the different kind of errors. A try(fn(), "my error message: %w")
might be useful though. But even then, the if err != nil
construct might be still preferable, because of shorter line lengths.
Frankly, I don't want an implicit return that try
provides. If we had generics, I would much prefer a solution that used monad-ish behaviour instead.
type Result<T> interface {
Expect(err error) T
OrElse(defaultValue T) T
}
func From<T>(value T, err error) Result<T> { ... }
To me, this is a lot cleaner than the builtin currently being proposed, although further changes would be required to the above since you'd have a proliferation of methods that returned (value, error) and Result
The current try
proposal, having no way to explicitly decorate the errors, doesn't meet my needs. I can't imagine ever using it. Frankly, it might as well be called code_smell
.
It might not make sense to change it, because the wrong problem is trying to be solved.
The code that we are familiar with is not error handling.
if err != nil {
return err
}
This is error nil
handling. At no point in this pattern is the value of an error handled.
If I were to demonstrate this in a different language, Ruby.
begin
some_method_that_raises_an_error
rescue => e # catch any exception
retry e # throw it up again
end
This relays the same behavior as the golang code. When we detect that an exception occurred and then reraise it. We just throw it up the stack.
In golang, we return
it.
Where is the actual _error handling_ occurring?
We've all had similar experiences of the failure of this pattern. For example, receiving a file not found
error and then spending a large length of time tracing the original _thrower_ of this error.
This is why I believe the try
proposal (and others) are faulting. We don't have a good pattern for actually handling errors.
I've see err.Error()
string checking, type assertions, etc. to actually inspect the error.
We need a pattern for this inconsistency. It feels like xerrs
might be solving this, but it also doesn't feel complete yet.
I support keeping err!=nil check as is.
Every time I dig into a sizable Go code base I ask myself how I might reduce some of the boilerplate. I always come back to:
The issue tracker is useful for many things, but one thing it is not useful for is a detailed discussion of a complex topic. The issue tracker provides no threading, and replying to a specific message is awkward. Since there is no actual proposal here, just a response to other proposals, I really strongly encourage you to take this discussion to the golang-nuts mailing list.
If I may, I believe this is the answer. This new error proposal is in direct conflict with the goals of the language.
The reason I love golang is because of its simplicity and clear use of control flow. One of the things I despise most about Java is the try throw construct. It's so disgusting. It encourages terrible error handling. Sending exceptions up the call stack is a horrible and disgusting method of handling control flow. On top, it encourages wrapping everything in a giant check and calling it a day instead of a self documenting and explicit handling of each error situation.
If err != nil encourages good error handling, is self documenting and encourages good documentation as to the specific case, and it's honestly one of the things I love most about go. Making this new control flow interrupt, using messy, somewhat ambiguous returns and parameters, and confusing semantics is not in the spirit of the language I've come to adore.
Verbosity is not a bad thing. Unnecessary verbosity is, but I'd argue that go's error handling is not unnecessary. It's part of the language's charm.
Couldn't agree more. The explicit error handling is one of the best features of the language IMO. I always feel like many who are bothered by it just aren't used to it yet.
It is not good for the issues are separated, but I'm thinking that two opinions are merged as one opinion in this case.
GitHub vote icons can not interpret the second.
The explicit error handling in go is one of the reasons why I love golang. I don't understand why any go developer would want it any other way. I think the proposal to add new syntax is mostly from people comfortable using syntax used in other languages. it may take some getting used to but it works perfectly once you get use to it.
I wrote #32811 and I support this proposal more... I'd rather just leave error handling alone. I think the emoji reactions to this proposal say a lot.
I personally agree with leaving err handling as it is. One of things I like about Go is that the language is minimal, and generally speaking has one way of doing things. By adding new syntax for error handling, we’ll create a world where x% of code uses the current method, and y% uses the new method. This will, among other issues already discussed, create inconsistent code bases. I personally don’t think the value of new error handling syntax is worth the trade offs, since I consider the existing syntax enough/sufficient.
As someone that is newer to Golang, one of the things that I find refreshing about the language is the explicit error handling. I've worked in Java, Ruby, Python, and Node pretty heavily, and dealing with errors is so much more onerous than in Go. I would rather see the clear 'path' of errors, than have it implied to me by some language construct that makes it more vague.
ˋreturn ... if ...ˋ suggestion from @andreynering is actually fairly smart imho. Keeps the code explicit (no hidden control flow break) while cutting down the boilerplate (one-liner).
Agree, leave if err != nil
alone.
I prefer the current format. It is clear and an easy pattern to teach. Bringing new engineers up to speed is simple as they can learn one simple pattern and repeat it. It also asks the users to at least consider the error in the current context, ensuring that at least the engineer is acknowledging an error can occur here and I need to think about what to do.
I wrote #32804 and I would much rather see things NOT change. If your code is long, its because it does a lot of stuff. If you have a lot of error handling code, it's because you're doing a good job of handling all your cases.
Please, lets not add things just for the sake of adding things.
I enjoy the simplicity of the error handling as is.
Expect is just an anagram for except, and I'd rather not use it. Thanks for starting this.
Please don't change my holy grail.
There was overwhelming community feedback requesting more streamlined error handling (from the annual survey). The Go Team is now addressing that issue.
@icholy Sure, but the current proposals leave a lot to be desired. They all seem to either obfuscate the error handling, revert to more try/catch/finally style implementations, bubble the error handling up out of context, or otherwise make it more complicated. Since Go is supposed to be a simple language, I think a lot of us were hoping for a simple option. I haven't seen any I personally like, so I think that the better option is to keep the current pattern.
One complaint was having to type it, but virtually every editor has shortcuts to insert code snippets, so it really isn't a big deal. Perhaps it is my own experience having used Go since pre 1.0, but I happen to like the simplicity and don't mind the redundancy.
@kevineaton you think try
is complicated?
I agree with this completely. I’m not even personally convinced we need to do anything - I agree the if err != nil
checks look awkward at first blush but I haven’t seen anything proposed that actually solves the issue without broadly violating the very things that go is popular for.
@icholy after spending ten years writing Java and Python prior to Go, I think it can be. I think you run into Pokemon exception catching, or catch chaining of multiple exceptions, and otherwise introducing even more overhead and boilerplate. I wouldn't go back to that style of error handling if I could ever avoid it, as it almost always led to headaches and confusion ESPECIALLY when teaching. I also teach computer science on top of my day to day work as a software architect, so I am biased towards teaching new developers and mentoring. I would choose Go and it's simple error handling over more potentially more complicated or nuanced error handling any day.
The issue tracker is useful for many things, but one thing it is not useful for is a detailed discussion of a complex topic.
Ain't _that_ the truth. But here we are.
if err != nil
won't go away if try
is added. I believe that try
will add some clarity to code paths which are either error forwarding heavy or where a lot of different errors can be summe up easy in one defer error handler. . I don't really see how try
encourages not handling errors that much more than a bunch of emptyif-err-return-err
. It's easy to ignore actually handling the errors regardless of try
is there or not. I think that try
is one of the best suggestions for error handling yet because it looks like it will be easy to read code which is using it..
My unsolicited two cents, it just doesn't feel very "Go" like. It's too magical and we're going for implicit constructs over explicit ones.
From the Go FAQ
Why does Go not have the ?: operator?
_There is no ternary testing operation in Go. You may use the following to achieve the same result:_if expr { n = trueVal } else { n = falseVal }
The reason ?: is absent from Go is that the language's designers had seen the operation used too often to create impenetrably complex expressions. The if-else form, although longer, is unquestionably clearer. A language needs only one conditional control flow construct.
@ianlancetaylor
The issue tracker is useful for many things, but one thing it is not useful for is a detailed discussion of a complex topic. The issue tracker provides no threading, and replying to a specific message is awkward. Since there is no actual proposal here, just a response to other proposals, I really strongly encourage you to take this discussion to the golang-nuts mailing list.
You can reply to a specific message. I just replied to yours. :)
Since there is no actual proposal here, just a response to other proposals,
A proposal to me means a call for change. This particular issue is anti-change. Do you propose that we create a proposal to _not_ change error handling? I think the proposal system is great but it leaves the status quo underrepresented.
after spending ten years writing Java and Python ... I also teach computer science on top of my day to day work as a software architect
@kevineaton are you done sucking your own dick?
This issue functions as a long ongoing poll in a semi-official place where basically anyone can easily vote for or against proposals.
Not changing the language to remove if err != nil
is a perfectly cromulent proposal that needs basically no added detail. I'm not sure what the problem is. No, it's not god awful long and hard to grok. That doesn't make it wrong or bad or insufficient.
+1, if nothing better, good thing will be a really good stacktrace info (without frames dance stuff), I guess x/errors
already achieve this, but, I would love something ala swift in the near future, like marking func
s using the throws
keyword that would return an error
+ try
keyword, preventing error var shadowing (which I personally hate), something like this:
func a() (int) throws {
throw &someError{}
}
anInt, err := try a()
@icholy That was incredibly uncalled for. This is a place for discussion and the Go community is supposed to be a welcoming community. There is no place for that kind of remark. I believe Socrates had something to say about insults in a debate.
The current error handling is human-error-prone. It is easy enough to forget to check err
at the moment. If there are any checks already in the scope (and most of the time there are), compiler won't terminate with unused variable
. Error handling should be strict - you either _
an error or check it - no leg-shooting should be possible.
@kevineaton you think
try
is complicated?
try
is a code smell. It forces indentation amongst your entire block of code instead of just in one spot. Furthermore the "bubble-up" nature of exception handling creates de facto nondeterministic behavior in code and multiple exit points.
The beauty of using multiple return values instead of try
is there is one value to check when your function is done and one point of exit from your function (unless, of course, using guard statements or other explicit returns).
try
blocks defeat the whole fucking purpose of multiple returns.
@fillest While it would make the code a bit less readable, I do think this would be a value add in terms of safety / explicit error handling. If you look back at the original goals for how we handle errors in Go, I think that would be a nice iteration to avoid the class of bug you cite while still pursuing the spirit of explicit being good.
The current error handling is human-error-prone. It is easy enough to forget to check err at the moment. If there are any checks already in the scope (and most of the time there are), compiler won't terminate with unused variable. Error handling should be strict - you either _ an error or check it - no leg-shooting should be possible.
@fillest The proposed change to error handling makes "leg-shooting" easier and errors are more pronounced because they can lazily be handled.
I stopped using Go because of the lack of generics, boilerplate propensity, GC, lack of resource limits/accounting and workload generated from PHP noobs who didn't understand what a compiler does. Haskell, C# and others solved error handling pretty well... the Go 2 proposal looks alright if it has explicit case handling as before (unsure).
Error handling is at the heart of programming. Modeling the business logic (however complex it is) is always simpler than responding to the invalid conditions this logic generates. Simply forwarding an error is a code smell. I wish Go doesn’t encourage this behavior but promotes error management patterns. Beginners often get confused with all this error handling code because they haven’t realized how central error management is.
Fully agree, since the try
builtin will not help to wrap errors and add information to them even for a bit.
Before rewriting with try
:
_, err := doSomething()
if err != nil {
return nil, errors.Wrap(err, "failed to do something")
}
_, err = doOtherThing()
if err != nil {
return nil, errors.Wrap("failed to do the other thing")
}
Imagine what will be after rewriting with try
.
Since try
already acts like a 1-argument function by enclosing its argument in parenthesis, it could accept a 2nd argument that is the error wrapping code.
try(extract_value(try(get_data(1), errors.Wrap(err, "failed to get data")), errors.Wrap(err, "failed to get data")))
Where the err
value would have to be implicitly introduced (in a hygenic way). Then if try
is used as a 1-argument function then it would just return its error unchanged.
I agree, the only "syntactic sugar" thing that might make error handling a little simpler is letting us do something like the following when we have multiple returns from our functions... underscores would just be default values of whatever the return types are
if err != nil {
return _, _, err
}
@sorenvonsarvort it doesn't seem that bad to me:
var errContext string
defer func() {
// err is a named return
if err != nil {
err = fmt.Errorf("%v: %w", errContext, err)
}
}()
errContext = "failed to do something"
_ := try(doSomething())
errContext = "failed to do other thing"
_ := try(doOtherThing())
By my understanding, you can also still use if err != nil { ... }
if it is clearer for that particular section of code.
try
shines in other cases. Imagine something more like:
func trySomeComplexOp() (r result, err error) {
a := try(step1())
b := try(step2(a))
c, d := try(step3(b))
return try(lastStep(c, d)), nil
}
Code like the above code can be a lot cleaner than if you had to sprinkle in if err != nil
blocks. Go is all about "linear readability" so I think try
does well towards that end.
There was overwhelming community feedback requesting more streamlined error handling (from the annual survey). The Go Team is now addressing that issue.
This is a vocal minority and I bet a good chunk of them don’t even use Go
@sirkon what are you basing that statement on?
@sorenvonsarvort it doesn't seem that bad to me:
Code like the above code can be a lot cleaner than if you had to sprinkle in
if err != nil
blocks. Go is all about "linear readability" so I thinktry
does well towards that end.
In Russia we call this «экономия на спичках». Use google translate to get a meaning.
For those in this thread that haven't already, I would recommend reading this comment on the original try
proposal issue. It discusses general error context best practice and how it might be expressed with try
.
I think that perhaps error context has become a bit dogmatized in the Go community. I know I've personally fallen for this and over-contextualized my errors, resulting in very long, repetitive, and hard to read messages. There's a lot of nuance regarding when to contextualize errors and when not to.
I like that try
is basically a shortcut and reduces some boilerplate code. But we lose the ability to wrap the errors with extra information. However, the following change might fix that:
f := try(os.Open(filename))
becomes
f := try(os.Open(filename), "open data file")
Of course, if you need to do a lot more than that, the "full" way of doing an err != nil
check is still available.
I agree with this, but i'm going to respect the request from the go team to have more experience with the change before having a final opinion.
But my preliminary experience with the change seems to support that it is really unnecessary. I have 2 "real world" programs with about 10k lines each and running tryhard on both shows that none of them would benefit from this change. This is easily explained by the fact that both always add context to errors. I have other smaller "toy" programs in Go and tryhard
did find 1 case where i could've used try
in one of them, but that's it.
I admit that other people may treat errors differently than me and i admit the possibility that try
can be used in a positive way. The tryhard
source code itself has some consecutive cases of return err
, that if it were to use try
i don't think legibility would be compromised that much. But i just fear the misuses, because those will impact legibility. A good example is provided here. And then determining what is a good use or not will be a whole other story.
Also, i like how people usually can just read go code even if they don't program go themselves. This will make them have to learn the magic try
does, specially because it does a different thing than the other try
they've seen in other languages. This is true also for new people coming to the language, it's just another feature they'll have to learn in a language that takes pride in being simple by having "just the features you need".
Let's wait and see. I'll be experimenting more with this change, but i'm not positive it'll change my position.
There was overwhelming community feedback requesting more streamlined error handling (from the annual survey). The Go Team is now addressing that issue.
@icholy Like i said, i do like to add context to errors. In that regard, "More streamlined error handling" for me means better ways to provide that context and to get information from it. For instance, with all the context i added to my errors should be trivial to ask "the context" if the error was caused by a timeout. But it's not. You usually have to resort to use pkg/error
, make your own "error structures" and/or make methods to work with them that depending on implementation can resort to do string searches. I'd rather see something that would save me from making whole structs and methods than things that save me a single if
very occasionally at best. And like it was previously said, can you really call it "error handling" when this change is basically providing a more convenient way to not really handle the error?
you think
try
is complicated?
In isolation try
is not complicated, but language changes are not considered in isolation. Consider:
try
because its shorter, in cases where if err != nil {return ... errors.Wrap() }
should have been used insteadI echo above sentiments that simplicity (having a single way to check errors) is more important than a brief way to check errors.
Is it not go's way of handling a err is through that global statement and if this is something to be ignored then How are panic situations handled ?? I would support for a better error handling as other programming paradigms handle errors today but not to ignore
I don't see any problem with try
proposal?
If u want to use old behavior or need other way handle error, why not re-use old way? No-one sticking a knife to your neck presses you use try
syntax?
try
syntax is simple, and a beatiful way to reduce boilerplate code, I don't know why gopher love sadism-way?
What if gofmt were changed such that one-statement if blocks stayed on one line?
That way, we could have wrapped errors take only one line to handle instead of three for most cases. This would reduce the vertical space taken up by error handling and make it seem like error handling isn't taking up over half of the vertical space in the average function.
Here's what it would look like:
// we already have an err in scope
err = frub.Confozzle(foo, bar, baz)
if err != nil { return errors.Wrap(err, "confozzling didn't work") }
I think that perhaps error context has become a bit dogmatized in the Go community. I know I've personally fallen for this and over-contextualized my errors, resulting in very long, repetitive, and hard to read messages. There's a lot of nuance regarding when to contextualize errors and when not to.
The error wrapping and stack frame/error printing stuff would make a lot more straight forward. I think that there will be a good solution for that in time but not right now.. Personally I would prefer to wait with introducing more powerful go2 features until parametric polymorphism is settled because it can potentially be useful when designing the rest of the features
I totally agree with leaving it as is. It's a little bit too verbose but it's pretty straight-forward to follow.
If I could just reduce
if err := foo.x(a, b); err != nil {
return err
}
if err := foo.y(); err != nil {
return err
}
if err := foo.z(c); err != nil {
return err
}
to something like
if err := check foo.x(a, b), foo.y(), foo.z(c); err != nil {
return err
}
maybe would also be great without changing the idiom too much IMHO.
@henvic I think the problem with that is that you are assuming you want to handle the three errors in the same way. Go forces you to think about how to handle each error individually. Having separate error checks makes this clear. Aggregating them into one makes the dev have to cognitively go back and check, should these errors _really_ be treated in the same way? I think with this proposal we loose clarity and forced thinking around errors for a few less key strokes.
@sanbornm, you are right. I agree.
This check
could too be a checkNonZero
operator that would only take one return argument and return at the first non-zero value (like null). But besides being too vague, and affecting the language even more. It would even only slightly give a hint that you shouldn't use it if expecting, say, io.EOF. Another possibility would be, perhaps, to count on go vet
to at least let you know of the most common cases (like io.EOF when reading from a pipe)... but this idea doesn't sound good at all to me.
I don't see any problem with
try
proposal?
If u want to use old behavior or need other way handle error, why not re-use old way? No-one sticking a knife to your neck presses you usetry
syntax?
try
syntax is simple, and a beatiful way to reduce boilerplate code, I don't know why gopher love sadism-way?
We all live in community. Every day I work with a lot of code written by other people. So change in language will affect me even if I don't use it.
In large projects, the worst logs are generated by a function we forgot somebody wrote, which calls a library which calls a library which calls a library which threw a generic new Exception()
that nothing caught until a "Pokemon" exception handler did and logged a generic error. The second-worst are the same, but with an inscrutable several-hundred-line stack trace, with which I guess we can eventually figure out the cause (luckily just searching for github.com/<us>/<ourproject>
finds most of the relevant information, but there's sometimes a lot). Despite their name, "Exceptions" are distressingly unexceptional in big Java projects.
Meanwhile, even when there's a lot of redundant context, simple Go error strings like "narf: Error unpoiting the zort: foo: Unexpected bar in baz: {\"ork\": \"morpork\"}"
have been (in my experience) typically very easy to interpret, as long as we've been diligent about embedding the important context somewhere in the actual error value. If important context turns out to be missing, that is _also_ usually fairly obvious. The "fix" in those cases is adding more context and waiting for another error, so it's not perfect, but on balance I still prefer this to scrolling through stack traces and/or relying on the dependencies of the dependencies of my dependencies to "throw" or "raise" sane error messages. I really appreciate how the name panic()
seems to prevent most Go developers from so liberally deploying what is essentially the same language feature. Let's not make error
and panic
the same thing after all.
I've occasionally run into situations where a function had a dozen or so failure modes, and the vast majority of those deserved the same error message. The repetition doesn't really bother me, but there's usually someone else on my team it does bother, so we compromise by declaring a closure at the start of the function to handle those common errors.
func foo(a, b, c SomeArgType) (x, y, z SomeReturnType, err error) {
handleError := func(handleErr error) (x, y, z SomeReturnType, err error) {
log.WithFields(logrus.Fields{
"package": "foo",
"func": "foo",
"arguments": map[string]SomeArgType{"a": a, "b": b, "c": c},
"error": handleErr,
}).Error("Error fooing the bar")
return reasonable, default, values, handleErr
}
err := doABunchOfThings()
if err != nil {
return handleError(err)
}
}
Which, admittedly, is _still_ an imperfect solution in some ways. But I like that doing this made it very easy for future developers to still understand when and what foo
returns, without control flow jumping around too much.
If somehow this repetition "problem" is extremely common throughout numerous packages instead of (as I usually see it) confined to a handful of irreducibly-complex functions in an irreducibly-complex package, a project-wide "functor" could probably be used to a similar end, and (sigh) if a concept of parametric types ends up getting added to the language, you could hide even more detail behind an error-handling-factory-factory without needing to rely on try/catch.
@thomasf
The error wrapping and stack frame/error printing stuff would make a lot more straight forward
I agree.
Personally I would prefer to wait with introducing more powerful go2 features until parametric polymorphism is settled because it can potentially be useful when designing the rest of the features
A few people in the original try
proposal discussed waiting for generics as well, but it's not clear to me how parametric polymorphism would make the try
proposal any different. It's already a built-in, so it's not confined to the limitations of what we can express in the language.
@icholy
There was overwhelming community feedback requesting more streamlined error handling (from the annual survey). The Go Team is now addressing that issue.
Just to respond to this; there also was a majority in the UK in favor of Brexit. Sure, the EU also brings about some disadvantages that the general public responded to. However, once all the alternatives were brought up, it seemed like staying in the EU wouldn't be so bad after all.
Now it's not at all my intention to politicize this, and you may disagree with the above. But what I do mean to show is that even when a majority initially considers something to be a nuisance, it still can be the best solution once all the alternatives have been examined.
I'm not strongly opinionated about the error handling, but it _could_ be an argument for leaving things as they are.
In a professional coding environment, we take advantage of current error handling practices to annotate tracing systems and decorate logs. As an aside, the implicit return is rather like using panic in an exported library function, in that it obscures the immediate flow-control readability.
@icholy
There was overwhelming community feedback requesting more streamlined error handling (from the annual survey). The Go Team is now addressing that issue.
Just to respond to this; there also was a majority in the UK in favor of Brexit. Sure, the EU also brings about some disadvantages that the general public responded to. However, once all the alternatives were brought up, it seemed like staying in the EU wouldn't be so bad after all.
You don’t need to consider this person’s claim seriously: Emojis’ count shows people are generally don’t like try
proposal and they generally like this “leave it as is” one.
PS in my practice a huge majority of people who dislike Go in its main domain (network services, CLI utilities) haven’t even used it. So I would prefer to ignore their opinions.
We need better, less controversial, options than the try
proposal.
I don't see the urgency for hasty solutions.
@velovix I think I hate parametric polymorphism more than try/catch error "handling", but if it did become a language feature, I could see a few ways it'd sidestep the need for another builtin language feature.
For one thing, when the code people don't want to repeat is boilerplate like:
foo, err := Foo()
if err != nil {
log(err)
}
bar, err := Bar(foo)
if err != nil {
log(err)
}
// ...
Then some combination of parametric functions, type inference, and _maybe_ or _optional_ style object design patterns would straightforwardly (heh) reduce the boilerplate without resorting to weird non-linear control flow strategies:
func<T> DoWithErrorLogging(f func(any...) (T, error), args... any) T {
t, err := f(args...)
if err != nil {
log(err)
}
return t
}
// ...
foo := DoWithErrorLogging(Foo)
bar := DoWithErrorLogging(Bar, foo)
IMO this all would be much, much worse than Go1. But better than having this _plus_ try/catch keywords in the language.
Honestly... As things stand right now, I think my favorite "breaking" changes for Go2 would just fix all the little inconveniences in Go1, like the net/http
defaults being mutable shared globals nested inside mutable shared globals (just make Hashicorp's cleanhttp
standard, basically), or (*time.Timer).Reset()
having a useless return value that you just have to know about, or the entire syscall package. Go3 can be released almost immediately after that with whatever tumors people want to grow on it; I don't see why small and big breaking changes need to all be done in a single release.
I'm in favor of try
... when used sparingly. I suspect that popular projects will add guidelines on when/if they're ok with use of try, and that small/new/single-person projects will sometime suffer from poor errors - and thus lack of use - due to try
ing too frequently. Those projects will die off or be fixed or forked.
I really don't see addition of try
to the language to be all that horrible. If people's worst-case fears prove founded, its use will be frowned upon. Inexperienced people will use it indiscriminately, while others won't. It's not the end of the world.
If try
is added, I'll probably use it in some cases. Those cases are, where an error is returned but I think it's so incredibly unlikely for there to actually be an error that I don't see the point in adding context, and I simply return the error as-is. For example, if I've just created a filesystem that fills a disk I know is 1 TB, I can be certain there's space to create a 1kb file or a directory. If that fails, I don't want to ignore the error - it could indicate a bug elsewhere, hardware failure, etc. However, it's not really worth putting effort into annotating every insanely-unlikely error.
I don't like how try(..) is just putting one more pair of parentheses/brackets on a coder's mind stack to think of when typing. And for the longest time I can imagine!
So this is better:
value, err := foo()
return err if err != nil
But still this is so common. So I would like if smt like this could be somehow possible:
value, check err := foo()
If Go wants to be a readable language, the ability to do thinks that are difficult to read or understand should be minimized.
If Go wants to have good error handling, it should encourage errors to have additional context as they ascend up the call stack. The requirement of using defer to handle errors seems confusing. What if your error handler has an error? Defers are executed in stack order, do we need to declare handlers in reverse?
If statements are straightforward, and leave little room for ambiguity here. I feel like try
is solving a nit rather than a real engineering problem. I like this proposal because it allows the silent majority to finally make a statement without fully understanding every facet of these complex proposals.
@icholy Please be polite. Please be aware of the Gopher Code of Conduct: https://golang.org/conduct. Thanks.
Everyone: in addition to my comment above (https://github.com/golang/go/issues/32825#issuecomment-506740412) please be aware of https://golang.org/wiki/NoPlusOne. It is not helpful to make a comment saying little more than "I agree" or "I disagree." Use the emoji buttons instead. Thanks.
@sanbornm
(I agree that it is possible to reply a message; I said it was awkward, not impossible. And my point about threading still stands, in that this mini-thread is lost in a blizzard of other comments.)
A proposal to me means a call for change. This particular issue is anti-change. Do you propose that we create a proposal to not change error handling? I think the proposal system is great but it leaves the status quo underrepresented.
It's not necessary to create proposal A saying that proposal B should not be adopted. Vote down proposal B instead. For detail discussion of proposal B, use that proposal or the mailing list.
(I understand that the proposal B in this case is locked; the fact that this proposal has 77 comments in less then a day shows why. This level of discussion simply works better on a mailing list rather than the issue tracker.)
@ianlancetaylor
(I agree that it is possible to reply a message; I said it was awkward, not impossible. And my point about threading still stands, in that this mini-thread is lost in a blizzard of other comments.)
Fair enough, that makes sense. Mailing lists are great but I personally find it easier to contribute through GitHub in this case. I don't have much to say other than the current error handling is great and I wish that it remains the same. Emoji/votes are great for this. You probably don't want 100 people writing "Please leave error handling alone" in the mailing list where 100 "votes" would suffice.
Because this issue is locked it can no longer be "voted" on with Emojis. Which is why I believe this issue was created in the first place.
A side point but related, dependency management wasn't handled well. Dep worked great and go mod was chosen (form what seemed) out of nowhere [1]. I understand it this is why the proposal system was created. I just feel like the proposal system in this case might under-represent the community if issues are locked and we are told to go to mailing lists.
[1] https://twitter.com/_rsc/status/1022588240501661696
Edit: The Go team and community for the most part do an amazing job listening to the community feedback. I appreciate all the work that goes into it. The Go surveys are a great example.
@sanbornm
Dep worked great
Need to disagree here. Go modules finally solved that ill known “gobindata” problem with their persistent caching by https://proxy.golang.org
That dep dude didn’t even realize the problem and was toying with fancy “ensuring” via VCSes instead .
@sirkon This is a little off topic but you don't need any of that if you vendor deps like Dep was doing.
As it stands I think I would rather leave things as they are unless more constraints were added, like 1 try statement per line. Reason is consider this example from the proposal - it seems harmless enough info := try(try(os.Open(file)).Stat())
but it leaks file handles beyond the scope of what normal control flow would. I think we will see an increase in file resource leaks with io.Closer
implementations or other cleanup function that people may evade in the pursuit of more compact code.
Maybe some people will consider it's inconsequential because f will no longer be live and thus eligible for GC immediately and at some point the finalizer will ensure that f is closed. I think that it changes the previous clear (linter supported) conventions of using defer today which are bound to a function block. When the function block is exited the resource is freed. Relying on the garbage collector provides no guarantees that you won't exhaust resources (typical open file handle limit defaults can be between 1k and 4k) - which is easily exceeded with a simple filepath.Walk that doesn't close the files it stats.
In summary I think this syntax as implemented offers a subtle risk in resource management in Go since it lacks the ctor/dtor and relies on lower level GC machinery far removed from code blocks to prevent resource leaks. Turning something that seems harmless into a potential error condition (too many open files).
var n int
for _, name in try(os.Readdir(...)) {
n += try(getSize(name))
}
func getSize(name string) (int, error) {
return try(try(os.Open(name)).Stat()).Size
}
edit:
For the constraints, I actually think if it was only valid on the right hand side of an assignment that would be better than saying 1 per line, since a, b := try(get("a")), try(get("b"))
is reasonable enough. But it still leaves the ability to do try(os.Open(name)).Stat()
- which if you were to make try() void, but only when not on the RHS of an assignment you're left with something that isn't very function like at all.
@cstockton wow great finding!
Rust actually has similar macro (?
if I remember correctly) which does exactly what this try
meant to do, but they have proper raii there, so it’s not a problem in that language and a huge hole in our case
@sanbornm yea, keeping half of the internet in your repo looks like a great idea indeed.
Since someone mentioned a utility that would count the number of places where try
would save me time/effort, I decided to run it over my largest work projects, plus everything else in my old GOPATH GitHub directory.
| Project | LOC* | try candidates |
|----------|------|----------------|
| cal1 | 2047 | 3 |
| pump1 | 1030 | 0 |
| docs1 | 4576 | 8 |
| hugoutil | 604 | 1 |
| everything else | 8452 | 23 |
cloc
utility.Bear in mind that the contents of "everything else" includes quick hacks and code I wrote when I was learning Go.
My overall conclusion is that for me at least, the try
proposal wouldn't help streamline my error handling to any worthwhile degree.
The biggest reason I love go is, its spec restricts coders to a small subset of syntax available to other languages. Because it's such a small feature set, it's easy to learn the entire feature set. A future developer can probably look at my code and know what I did. Each new thing added to the language decreases the chance that future developer knows that thing. The extreme of the slippery slope is a language whose complexity makes it hard to grok, like C++ or scala.
I'd like to see no syntax additions to go 1. Put them in go 2 instead.
@miekg please add this link https://github.com/golang/go/issues/32825#issuecomment-506882164 into the proposal. The example completely disqualifies the whole idea of this recent try
keyword.
I totally agree with leaving it as is. It's a little bit too verbose but it's pretty straight-forward to follow.
If I could just reduce
if err := foo.x(a, b); err != nil { return err } if err := foo.y(); err != nil { return err } if err := foo.z(c); err != nil { return err }
to something like
if err := check foo.x(a, b), foo.y(), foo.z(c); err != nil { return err }
maybe would also be great without changing the idiom too much IMHO.
If you were talking about a "Maybe" type, it requires variant type first.
Let's keep if err != nil
it works, it's clear, it's really not that verbose, and it makes sense in the flow of the code. As you read code with this construct you know what it is going to do.
Let's keep it, let's not add try
When I read code, I want the lines doing the job to be clearly readable, without or with a minimum of error handling stuff.
The 3 letters 'err' at the same level are acceptable to me. I would not like some 'check' function wrapping the important code, because the important code would be at a second level (remember lisp?), and I would not like a 'try' a line before, because the important code would come indented and on the second line.
res, err := begin_job()
if err != nil {
handle_error()
}
err = continue_job(res)
if err != nil {
handle_error()
}
With this code, you can read the flow of the non-error case by reading the first lines of blocks (as I read the titles of docs when I have to read it quickly)
Since someone mentioned a utility that would count the number of places where
try
would save me time/effort, I decided to run it over my largest work projects, plus everything else in my old GOPATH GitHub directory.Project LOC* try candidates
cal1 2047 3
pump1 1030 0
docs1 4576 8
hugoutil 604 1
everything else 8452 23
- Go code only, excluding comments, according to
cloc
utility.
I think that try
is more needed in larger programs. Just drawing from memory I think that programs of with LOC sizes around 15-20k and upwards need them more because that is when you might start to get layers which just need to pass on errors because they are aptly specified and handled in a closed system by both the sending and receiving side. It depends a lot on which sort of program it is though. I probably would not use try much in smaller programs either
I think that try is more needed in larger programs.
Good point. I tried tryhard on heptio/contour, 28.7k lines of source text, tryhard found 12 substitutions.
I think that try is more needed in larger programs.
Good point. I tried tryhard on heptio/contour, 28.7k lines of source text, tryhard found 12 substitutions.
WOW! 12 vs 28.7K lines, this really needs dedicated keyword!
Well, I am more interested in your POV on this:
stat := try(try(os.Open(fileName)).Stat())
I think it's more common if your program is a bit more monolithic and not part of a service integration between many services. When i just search for fmt.errorf
or errors
in github on that repository (heptio/contour
) there is only very few results so it's hard to get a quick overview.. But as I said it probably varies a lot from program to program, even for larger programs.
Say that you have a single program which does not use a lot of external libraries. Then you can have a specific AuthorizationError (and you know all errors returned are specific enough with any io errors already handled and wrapped) which already contain your user meta data and can be propagated without change a few layers without much change to things that actually need to handle them until the request layer.
I think it's more common if your program is a bit more monolithic and not part of a service integration between many services. When i just search for
fmt.errorf
orerrors
in github on that repository there is only very few results so it's hard to get a quick overview.. But as I said it probably varies a lot from program to program, even for larger programs.Say that you have a single program which does not use a lot of external libraries. Then you can have a specific AuthorizationError which already contain your user meta data and can be propagated without change a few layers without much change to things that actually need to handle them until the request layer.
You don’t get an idea. Annotations are to easily find a way error occurred. We similarly have os.NotExist
but this is barely a good hint on error path.
@thomasf here is another data point, from a several year old working copy of juju/juju,
529628 source lines, tryhard found 1763 (0.3%) replacements.
Yeah, sure. Since you have been involved with both they are probably not great examples of different ways to write programs though. I don't have time to even try the tryhard program atm and even less doing a proper run of it on varied sources (which might be impossible to collect anyway because it omits closed source code if being collected via github)
529628 source lines, tryhard found 1763 (0.3%) replacements.
As someone (citation needed) wisely said, try
doesn't make it easier to handle errors. It makes it easier to not handle them.
If you analyse code and find lots of try
replacements, all that tells you is that the code does nothing with errors except return them. That's probably not great code. Should we making it easier for people to be lazy and not worry about errors? Isn't that one reason Go doesn't have exceptions, to avoid precisely that?
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
If you analyse code and find lots of
try
replacements, all that tells you is that the code does nothing with errors except return them. That's probably not great code. Should we making it easier for people to be lazy and not worry about errors? Isn't that one reason Go doesn't have exceptions, to avoid precisely that?
try
is easier to misuse than what we have now, go has no features to enforce error handling and it's very easy to skip it already. For me try
is about making code easier to read IF it does not need to handle errors.Maybe there isn't a need for try
because it won't bee needed in many places, but let's entertain the idea and come up with uses cases instead of just being grumpy about it...
I can't think of many practical situations myself how I would use it but it also does not exist yet so it's hard to tell if I would design some things differently if it did exist.. One thing that comes to mind is that I might do is group a number of actions in an anonymous function like this using try and still handle the error before returning it the the caller. It could make some code a lot more readable.
var v1, v3 string
if err := func() error {
try(onething())
v = try(twothing())
try(otherthing())
v3 = try(somethingg())
}(); err != nil {
... handle error...
}
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.
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.
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.
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.
@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
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.)
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.
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.
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 simplifiesif 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).
@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.
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.
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.
@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).
@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.
@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 togolang-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
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.
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
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?
@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.
how about this syntax :
# call error handler
try callFunction(), errorHandler()
# error handler with anonymous function
variable := try callSomething(), func(err *Error) { # error handling }
@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.
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 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.
@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.
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:
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 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?
Some clarifications:
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.
I agree with @ianlancetaylor that this discussion is better held elsewhere (golang-nuts). There is no proposal here.
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.
@griesemer try is too error prone: there’s no RAII in Go, so we can’t just leave the function in many cases.
@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, I'm not sure how RAII is relevant to this discussion.
try
replaces existing patterns ofif ..., err := f(); err != nil { return ..., err }
with a... := try(f())
. If there was a resource freeing bug by usingtry
, then it certainly existed beforehand as well. The introduction oftry
neither enhances nor prevents the resource freeing bug.
Read the thread, there was an example:
info := try(try(os.Open(fileName)).Stat())
@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.
[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).
@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.
@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.
@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).
@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.
@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?
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()
}
@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.
@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.
@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.
@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 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.
@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 makingcatch()
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 thetry
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.
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
.
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.
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.
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?
@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:
try
as a name for the feature has been contentious, as long as we implement this as a function we are stuck giving it a name that I'm not sure anyone will feel 100% good about.try
does an unprecedented number of things, it has input like append()
and affects control flow like panic()
while taking the place of a ubiquitous pattern (if err != nil
).try
nesting is a consequence of the decision to implement it as a function, not as a value-add derived from rigorous technical debate.try
is implemented as a function to maintain backwards compatibilityI 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?
I like the error checking as-is, but if this is truly an issue that must be solved, I think a new keyword is probably a better solution. Any solution will likely involve control flow changes, and it's best to make that as obvious as possible.
In this example, the on
condition is evaluated every time err
is set.
func example() (foo int, err error) {
on err != nil {
return foo, err
}
foo, err = calcThis()
foo, err = calcThat(foo)
return
}
This works without declaring names for return values in the function signature, as well.
func example() (*int, error) {
var err error
on err != nil {
return nil, err
}
foo, err = calcThis()
foo, err = calcThat(&foo)
return &foo, nil
}
This could also be set multiple times. Here's a contrived example:
func example() (*int, error) {
var err error
on err != nil {
return nil, err
}
foo, err = calcThis()
on err != nil {
return &foo, err
}
foo, err = calcThat(&foo)
return
}
To my eyes, this retains Go's style and readability, while making it clear what is happening at each step (once you understand the paradigm), since it is effectively inserting that condition after each occurrence of err
being set.
You could even do the following:
func example() (foo int, err error) {
var message string
on err != nil {
return foo, errors.Wrap(err, message)
}
message = "failed to calc this"
foo, err = calcThis()
message = "failed to calc that"
foo, err = calcThat(foo)
return
}
Finally, this has applicability beyond error handling. Want to return if foo == 0? Another contrived example:
func example(i int) bool {
on x == 0 {
return false
}
x = calcSomeInt(i)
return true
}
@cstockton
I was about to protest that it'd be a little too easy to elide errors that way, but:
json.Unmarshal
. IME good code reviewers learn to look out for this pretty quickly.http.Handler
methods.So, after weighing all the pros I can think of, and failing to notice any obvious downsides, I'm... on the fence. I suspect there are non-obvious downsides I haven't considered. But I'm starting to like where this is going, for however little that's worth to anyone.
I hope this is the right way to respond and I am not committing a
serious faux-pas.
On 7/1/19, Chris Stockton notifications@github.com wrote:
@ianlancetaylor Has anyone suggested augmenting the ":=" operator to support
"inferred returns" - Basically behave exactly as try without a function
call. [ ... ]
What I find intriguing in this case is that we have something
analogous to the "comma OK" paradigm, where now omitting the "err"
assignee is permissible under some well-defined circumstances. Worth
noting, but clearly not in itself sufficient to make this a valid
proposition.
Lucio.
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.
@griesemer I don't agree. Although it can save keystrokes, pointer arithmetic was excluded from Go on the premise that it makes it easy to write bad, broken code. I feel that this is the same type of feature that will make bugs more difficult to detect.
data := try(ioutil.ReadAll(try(os.Open("foo.txt"))))
The typical example with try
uses examples like the above, which to me obviously leaks a file descriptor. (The fact that Go hunts down and closes such descriptors in the runtime unbeknownst to users in the current implementation is something that might give us comfort, but there's a better example anyway).
data := try(ioutil.ReadAll(try(http.Get("http://example.com/")).Body))
The above reads like correct code, but ignores the requirement that the response body be read in full and then subsequently closed on the happy path. If you look at it long enough, the disgusting elegance of the incorrect example should make it apparent that we will be seeing these types of bugs in the future.
As someone who reviews more than writes code at this point, I would prefer that Go not add in features that make incorrectness so tempting.
@jesse-amano Using the assignment operator actually prevents this case from being possible, without an explicit assignment statement the below behave exactly as they do today, i.e.:
json.Unmarshal(...)
(http.Handler)(h).ServeHTTP(w, r)
As for Values that just return an error are eligible for being returned like return json.Unmarshal(...)
and can also be represented in the more compact form since there is no need for a value to exist outside the if block.
if err := json.Unmarshal(...); err != nIl {
return err
}
What I find intriguing in this case is that we have something analogous to the "comma OK" paradigm, where now omitting the "err" assignee is permissible under some well-defined circumstances. Worth noting, but clearly not in itself sufficient to make this a valid proposition.
The behavior would be identical to try without parens or arbitrary nesting & chaining. I think it will be hard to find something that a majority of people think feels natural without breaking the language. I admit since it seems the Go authors are pretty determined to add this type of feature to Go I am really digging deep for alternatives because I am absolutely convinced try
in it's current form is not a good fit for Go. This is the closest thing I can think of that won't break BC, but maybe it still doesn'tt feel right to enough people. Either way I hope the try proposal is denied or someone comes up with something a lot more people can agree on.
edit: @jesse-amano I completely missed your point sorry! I suppose being inside a function that doesn't return an error would display a typical assignment count mismatch compile error? I imagine try would probably introduce a new type of compiler error message for that.
@beoran Regarding https://github.com/golang/go/issues/32825#issuecomment-507126700 : Error handling is already different from situation to situation: Sometimes we return the error unchanged, sometimes we return a decorated error, sometimes we act upon an error, and sometimes we (can correctly) ignore an error. The only thing they all share (except when we ignore the error) is the err != nil
test (which we already can do in more than one way). As much as it would be nice for a new language feature to capture all these cases (or 95% of them), such a construct is highly likely to interfere in non-orthogonal ways with other control constructs we already have. That is, the best we can hope for is making some of these cases (maybe 20%, maybe 50% of them) better.
@cstockton Regarding https://github.com/golang/go/issues/32825#issuecomment-507136111: If nested try
's are the only road block left and go vet
is not good enough, I think we can consider disallowing nested try
's - that would be easy enough. But at the moment I think we're still in the FUD (fear, uncertainty, and doubt) phase, with practically nobody having actually experimented with try
seriously. I note that the people who have done so, have reported positively.
PS: The try
issue is open again for feedback. Let's continue over there.
@cstockton Oh, absolutely. To clarify, the point I was going for was that it's _already_ bad practice to call functions like json.Unmarshal
without capturing the error value in the majority of cases, but usually not considered bad practice to defer file.Close()
instead of defer func() { err = file.Close(); if err != nil { ... }; }()
, and we learned to tell the difference pretty easily. So it shall (probably) be with your proposed change. I initially flinched at the thought of somebody innocently using foo := strconv.ParseFloat(bar, 64)
when they intended to handle the error, but after brief consideration I don't really think it's a problem after all.
@sirkon Regarding https://github.com/golang/go/issues/32825#issuecomment-507167388 : Please leave such clearly unqualified statements out of the discussion - they have no place here. Just for the record, many ideas in Go are actually from the 80s (packages, separate compilation, etc.) rather than the 60s and many are much younger (goroutines, interfaces). These ideas may still appear old, but they have stood the test of time (at least the little time CS has been around).
@turtleDev Regarding https://github.com/golang/go/issues/32825#issuecomment-507231353: Go does exception handling, and it's done with panic
and defer
and recover
- we just don't call it "exception handling" because that term comes with a connotation that is misleading for Go. But just to be clear, Go can do all that raise
with try-catch
can do, and more (because in contrast to try-catch
, defer
can be used conditionally). Thanks.
@sorenvonsarvort Regarding https://github.com/golang/go/issues/32825#issuecomment-507268293: If you want different error decoration for each case, use an if
statement. Please see the design doc. This question has been answered many times by now. Thanks.
@cstockton Regarding https://github.com/golang/go/issues/32825#issuecomment-507306652: Yes, we have thought about such a mechanism. Specifically, we have thought about "turning the tables" and instead of providing try
, just provide handle
, which declares an error handler. If a handler is present (and only then), one would simply leave away the err
in the LHS of an assignment, and it would be checked implicitly (like with an invisible try
). It does look nice, but it's also completely invisible, which is a big red flag. We do want exception handling to be explicitly visible in the code, at every place. Without that, it would be almost impossible to read code and see where error checking is happening.
@griesemer thanks for clarifying. but panic and recover have different use cases and for most part are very hard to find in any production code base. that leaves you with only a limited amount of control of flow constructs. adding a new construct would break that consistency as you now have a new control of flow construct that does something like return.
@matthew-noken Regarding https://github.com/golang/go/issues/32825#issuecomment-507323973: You are proposing an interesting idea; it looks very much like a debugger's watch point mechanism. There are some questions that would have to be answered: Does the on
block have to return (I suspect so, because otherwise you get into spaghetti code land)? Can one have more than one such on
statement? How complicated can the on
condition be (It will have to be evaluated upon each assignment)? Note that we cannot have arbitrary expressions because we have to identify the variable uniquely with the on
statement. Also, somewhat an anathema in Go: The on
construct implies invisible code to be executed elsewhere.
If you want to explore this more, I suggest to discuss this elsewhere (golang-nuts, or a different, new proposal). Thanks.
@as Regarding https://github.com/golang/go/issues/32825#issuecomment-507345821:
pointer arithmetic was excluded from Go on the premise that it makes it easy to write bad, broken code
Actually, it was excluded because it would have made garbage collection difficult or impossible (and yes, one can also write unsafe code). But the more important point here is that there was concrete evidence and experience that supported this decision.
There is no experience and no evidence yet that nested try
's are going to be prevalent or common. But see https://github.com/golang/go/issues/32825#issuecomment-507358397.
@turtleDev Regarding https://github.com/golang/go/issues/32825#issuecomment-507368167 : A panic
is _exactly_ an exception, and recover
inside a deferred function is essentially a catch
. They may be harder to find in production code because in Go we do not recommend you write your code using exceptions; they should only be used in exceptional circumstances.
Regarding the number of control flow structures: The proposal is very clear that try
is simply syntactic sugar, nothing else.
I've tried to answer some of the recent comments in this thread on this thread. But let's continue new comments on the try
proposal in the actual try
issue #32437 (now unlocked again as of today); and leave this issue reserved for the leave err != nil alone
discussion. Thanks.
@cstockton Another comment about https://github.com/golang/go/issues/32825#issuecomment-507306652 : If we implemented this, then starting with
func f() int { ... }
...
x := f()
and changing to
func f() (int, error) { ... }
would mean that the behavior of x := f()
would suddenly and silently be very different.
I ran some experiments similar to what @lpar did on all of our go repositories. The results are in this gist: https://gist.github.com/freeformz/55abbe5da61a28ab94dbb662bfc7f763
@ianlancetaylor I actually feel this would work out very nicely in most cases, and make introducing better error reporting much less impactful. Consider full examples for the two main cases, first the caller returns an error:
func f() int { ... }
func a() error {
x := f() // if f() is updated to return an error, we get automatic error propagation by default
...
}
func b() {
x := f() // if f() is updated to return an error, we get the same compiler error
// assignment mismatch: 1 variable but pkg.f returns 2 values
}
I think in the common case this is a nice benefit to this approach, the corner cases where this creates a problem I believe are already brittle. Only one I can think of that could be truly nasty is deadlocking a mutex:
func (t *T) a() error {
t.mu.Lock()
x := f() // if f() is updated to return an error, we get automatic error propagation by default
if x > 15 {
t.mu.Unlock()
return errors.New("t > 15")
}
...
}
But I think code that is written like that is already susceptible to deadlocks if it relies on a foreign library call to succeed to have valid program state. If it is stored in a scope that can live beyond panic then it can deadlock if the same library introduces a runtime panic via NPE. In addition a main motivator for writing code like this is the historical cost of defers living on the heap. With the performance improvement of single-defers living on the stack such code isn't really necessary. I think any derivation of this type of error is easily remediated.
Finally like the arguments of supporting the shortcoming of "try" with tooling can also be applied here. Given the increased adoption of go-modules we have a nice opportunity to inject a "library-upgrade linting" step to put such changes in front of a users face clearly.
@griesemer
Regarding the number of control flow structures: The proposal is very clear that try is simply syntactic sugar, nothing else.
Pardon me, but try
isn't going to be a macro (like C) so in effect, to the end user it's just another control of flow statement.
I believe I have no objective arguments at this point, so I'll concede that we maybe need better error handling, but I feel like try
may not be the best solution.
Again, these are my opinions and I'm just representing them. I'm sure the Go team has put much more thought into this than I ever will.
Side: It strikes me as strange that this issue has 1335 upvotes while the try
proposal (#32437) has only 279 downvotes. I would expect people upvoting this to downvote the try
proposal so that the community's feelings about it are more apparent, because these two proposals are mutually exclusive.
@griesemer
Error handling is already different from situation to situation: Sometimes we return the error unchanged, sometimes we return a decorated error, sometimes we act upon an error, and sometimes we (can correctly) ignore an error.
Agreed there, that much is obvious.
The only thing they all share (except when we ignore the error) is the
err != nil
test (which we already can do in more than one way). As much as it would be nice for a new language feature to capture all these cases (or 95% of them), such a construct is highly likely to interfere in non-orthogonal ways with other control constructs we already have. That is, the best we can hope for is making some of these cases (maybe 20%, maybe 50% of them) better.
The proposed try()
statement also "interferes" with if
and return
in non-orthogonal ways, so I'd say that that argument is not correct. Some people here dislike try()
for that very reason, but I disagree. Go is not Oberon, it is simple but not minimalistic, Go is more practical.
Where we disagree is that it is even worth while to bother with a language construct that, as you admitted yourself, has only limited use and applicability, and that can already be done correctly with if
and return
. I think many people, like myself, who gave their thumbs up to this thread are so underwhelmed by try()
that they rather would not have it at all. Even if it not orthogonal with return, a more powerful, more generally useful try()
is probably what most Go programmers would like to see.
@beoran,
You wrote that you would like a "more powerful", "more generally useful" try()
.
@griesemer mentioned 4 situations:
try()
solves 1 by design: it's literally a shortcut for if err != nil { return ..., err }
.
Existing language constructs solve 3 and 4. We can already act upon an error with if err != nil { ... }
and it will be difficult to find a more concise structure in that case. We can already ignore an error with _
.
This leaves us with 2 (return a decorated error). The try()
proposal suggests to use a defer statement to decorate the error, or if each error must be decorated differently then use a standard if err != nil { ... }
construct.
The reasoning is well explained in this part of the proposal:
Our first iteration of this proposal was inspired by two ideas from Key Parts of Error Handling, which is to use a built-in rather than an operator, and an ordinary Go function to handle an error rather than a new error handler language construct. In contrast to that post, our error handler had the fixed function signature
func(error) error
to simplify matters. The error handler would be called bytry
in the presence of an error, just beforetry
returned from the enclosing function. Here is an example:handler := func(err error) error { return fmt.Errorf("foo failed: %v", err) // wrap error } f := try(os.Open(filename), handler) // handler will be called in error case
While this approach permitted the specification of efficient user-defined error handlers, it also opened a lot of questions which didn’t have obviously correct answers: What should happen if the handler is provided but is nil? Should
try
panic or treat it as an absent error handler? What if the handler is invoked with a non-nil error and then returns a nil result? Does this mean the error is “cancelled”? Or should the enclosing function return with a nil error? It was also not clear if permitting an optional error handler would lead programmers to ignore proper error handling altogether. It would also be easy to do proper error handling everywhere but miss a single occurrence of atry
. And so forth.The next iteration removed the ability to provide a user-defined error handler in favor of using
defer
for error wrapping. This seemed a better approach because it made error handlers much more visible in the code. This step eliminated all the questions around optional functions as error handlers but required that error results were named if access to them was needed (we decided that this was ok).[...]
If we determine down the road that having some form of explicitly provided error handler function, or any other additional parameter for that matter, is a good idea, it is trivially possible to pass that additional argument to a try call.
Do you disagree with this reasoning?
I know this part of the proposal and I disagree with it, because the fact that questions were opened due to the idea of passing an error handler, doesn't mean we don't need such a feature. It just means we have to think well to answer those questions in a reasonable way.
Furthermore, now, we handle all 4 error use cases with an if err != nil
statement, which is widely understood, consistent Go idiom. Only using try()
for case 1, and possibly for case 2, if we don't mind the overhead of doing error wrapping in a defer statement, means the code for error handling will be split into if
and try
inconsistently, and if the error handling changes, we will have to switch between the one and the other.
A more powerful version of try()
, that could be used in all cases would allow us to use try()
almost always, and this would then become the new, consistent error handling idiom for Go.
However, with try()
as it is now, it is not widely applicable enough, and there is just not enough convenience gained to introduce the aforementioned inconsistency in the error handling into our code bases. That's why I feel underwhelmed by the current try()
proposal, and think doing nothing is better.
@beoran I think cases 1 and 2 have in common to return an error from the function (respectively without and with decoration), while cases 3 and 4 don't (respectively act upon an error and ignore an error). I think 'try()` focus is on cases 1 and 2.
What if the try()
proposal could be improved to handle cases 1 and 2, by accepting an optional handler function? Of course, this requires to find reasonable answers to the questions listed in the proposal, but it seems tractable. Would you support something like that?
Here me out. If the case for this is we want users to check errors, we should probably add checked errors (kind of like checked exceptions). That way we're explicit as possible and the user knows they're checking all errors.
In all seriousness, if I were the one behind making these decisions, I would really like to repurpose the Kotlin ?
operator or something like the rust unwrap()
behavior.
Either of these I think would be an improvement:
getFile()?.getPath()?.toString()
where you get a nil
back if there was an error along the way or
get_file().unwrap().get_path().unwrap().lossy_to_string()
where it blows up halfway through if there is an error. Rust deals with the second by having an expressive match
function that lets you do an exhaustive search of the errors and handle each separately, all returning a value of some sort up the chain.
On 7/2/19, Nicolas Grilly notifications@github.com wrote:
@beoran I think cases 1 and 2 have in common to return an error from the
function (respectively without and with decoration), while cases 3 and 4
don't (respectively act upon an error and ignore an error). I think 'try()`
focus is on cases 1 and 2.I'm somewhat disappointed not to see more discussion of my suggestion
that it is the "return" part of "error return" that needs to be
addressed, not the "error" part.
Then again, maybe I should have followed a more official approach. I'm
simply no good at formulating proposals, I tend to go all over the
place.
Cases 1 and 2 are, in my opinion, better served by a "fail" command
keyword that clearly indicates (after some getting used to it) a
change in the program flow and which is not subject to any
inconvenient tradition as regards its full syntax.
What follows from that, however, whether we like it or not, is that
the positioning of "err" as the last return parameter is soon to
become law rather than convention. That is one of many "unintended" or
"collateral" consequences that need to be taken into consideration.
There will be many other such consequences, from minimal to
disastrous, some that only opportunistically piggy-back on the
proposal. I would personally err on the side of caution, I do
understand why the Go Team and Robert in particular are seeking
support and no doubt are hurt that resistance turned out to be so
great.
It's a clash of political ideologies, perhaps we need to dig a lot
deeper to seek the real roots that need some gardening attention.
@lootch Do you have a particular concern with error needing to be the last return parameter for try
to work? Isn't that already a de-facto convention?
(As an aside, we're not "hurt" about the resistance - mostly flabbergasted about the outsize reaction.)
@griesemer sorry for the hoary car analogy but the proposals feel like "We've decided we'll add either a diesel fuel system to all gasoline cars, or a battery and electric motor to all gasoline cars." Go's error handling is good enough. I'm concerned that the value added will be less than the costs, including new footguns and mental overhead.
I just want to write a quick comment saying that having written a lot of Go (I have been using Go since its first public release in 2009 -- see my github) I would welcome improve ergonomics around error handling. While the explicitness of Go's error handling is nice it is also a pain in sequential code. The lack of introspection and typing around the actual values themselves (which is being addressed in a different proposal) does not in my experience actually improve the resiliency of the program.
I got annoyed enough at this few years ago I actually wrote some sugar around panic and recover to allow them to work (mostly) like unchecked exceptions: https://hackthology.com/exceptions-for-go-as-a-library.html . Actually using those wrappers is a bad idea because of were the community is at. However, I continue to believe improving the ergonomics around error handling is a win.
It seems crazy that people are ardently arguing for keeping an extremely verbose but unhelpful way of propagating error conditions. I have written and found real bugs in my code (and other's code) where people have messed up the if err != nil
condition and created bugs. There is really no reason for those bugs to ever get written. Static checking can help but not eliminate these bugs.
I support the proposals to improve the ergonomics around error handling.
The proposal process is for, to quote https://golang.org/s/proposal, "Proposing Changes to Go."
This issue is not proposing a change, so really it is a bit of a category error.
If someone filed a proposal titled "leave select alone" we would just close it as not a proposal.
But really this issue is just an extension of the discussion of other issues like #32437, so I will leave it open as a good place to discuss "do nothing".
I've marked it Proposal-Hold to indicate that there's not a specific decision here, more of a meta-decision.
Side: It strikes me as strange that this issue has 1335 upvotes while the
try
proposal (#32437) has _only_ 279 downvotes.
The try proposal was locked by the time I became aware of it, so I couldn't downvote it. I assume that's also why this proposal was entered to start with.
On 7/2/19, Robert Griesemer notifications@github.com wrote:
@lootch Do you have a particular concern with error needing to be the last
return parameter fortry
to work? Isn't that already a de-facto
convention?I do in the sense that if that became "de facto" cast in stone, then
that door should be swung open and make "error" an optional, special
argument type and accept that, because for every right outcome there
are more often than not many wrong ones and they may as well be
addressed in a special way.
Once the idea that the "return" statement needs to be investigated
more deeply (and the try proposal seems to be hinting in that
direction), then the error condition is no longer "merely a value, and
Go's approach starts to resemble the best house of cards built to
date, but a house of cards, nevertheless.
I am a reluctant user of a few of Go's cleverer tricks (I have
mentioned elsewhere that x++ and x-- do not feature - mostly, I slip
up sometimes - in my code) so "try" will remain one of those to me,
but I do not on principle object to its introduction, I merely feel
that so much has been said, and still all possible downsides may not
have been revealed (the unintended consequences of what I see becoming
an eventual political decision). Putting that to the test may be good
or bad.
What I see is that panic/recover got a bad rap and the injustice of it
is coming back to haunt us. Again, I have yet to use either and I
dread the day I have to because I'm not the sharpest cookie in the jar
and I will find it very hard, but had panic/recover been encouraged
for the rare occasions where it is suitable (none of them treated by
Rob Pike in his wonderful presentation about errors being just return
values, if I recall correctly), it would be much clearer to all that
Go already handles errors as well as can be expected without the need
to explore the swamp of possible options available beyond its
boundaries.
That said, it makes as much sense to put try to the test, it is, after
all, an optional feature. But the one side-effect is what this
exchange is about: what will the consequences be of "de facto"
compelling error handling functions to have an "error" type argument
at the end of their return parameter list? How will that change the
perception that "errors are just values"? How will it dovetail with
the now much more analogous "comma-OK" paradigm? What else will this
new principle give rise to?
Most of all, I think the mountain being made out of this molehill is
indicative that Go has reached a life-changing milestone. Almost
certainly, assumptions that were true in 2013, may well no longer
hold. Not that that is news to anyone, but it suggests that Go2 may
not be as wonderful to be backwards compatible with Go1 as has been
too firmly proposed (in my opinion).
@lootch Thanks for your detailed response. It is indeed very difficult to extend an existing language in a backward-compatible way, and so I'd agree with you that if that backward-compatibility were not required, perhaps one might do things differently. We will soon (once modules have become common place) be able to make language changes that don't require strict backward-compatibility.
As you say, try
is an optional mechanism, and - I think it's worth repeating - despite all the hoopla around it, a very simple mechanism at that, easily explained using existing Go functionality - it's just that we can't write try
as a function in Go ourselves (and generics wouldn't help either, for that matter). If we could, I'm certain that it would be quite common to see helper functions such as try
for error handling in existing code. It is after all exactly what Rob Pike's talk about errors being values is all about: it's programming with errors.
It is still very surprising to me that there's such an uproar regarding adding a pretty minor helper function that everybody is free to ignore, yet people are seriously contemplating significant language changes such as on error
which really do strongly suggest a specific style of error handling in the language.
Thanks again for your input. This is all very interesting.
I'm not sure if this proposal is the right place to discuss it, but given there's already a vivid discussion on the try keyword in this thread, I'll just consider it on-topic for now :)
I wonder if the Google folks would be willing to implement the try keyword in their internal Golang repo, and subsequently convert the existing Google code bases to use that keyword. By keeping it internal only, it would allow to revert it easily (that is, the burden is on a single company, rather than the entire community).
This would allow to do a little case study on this feature in a real-world high-sloc code base. Facebook has done something similar with PHP features in the past, and that way they were able to fine tune certain functionality before proposing it to the community at large.
If you were to write up a case study of how the try keyword was used _in practice_ and what value it added in real life, that could provide a compelling case. If it (hypothetically) wouldn't provide any real world value, that would be valuable to know as well. Sometimes something looks really good on paper but doesn't work out in practice - or vice versa.
@Freeaqingme We have already produced a tentative CL that shows how try
might look like in the std library: https://go-review.googlesource.com/c/go/+/182717 (there may be false positives, but if there are, they are very rare). We are contemplating perhaps developing a tool that permits conversion of code bases in both directions.
You are also encouraged to use tryhard
in your code base and report back.
Thanks.
@griesemer I may not have made myself clear. I assume Google uses their own build of Go internally. My suggestion would be to apply the patch you linked above to the internal Google build, and then convert (parts of) the Google code base - not just stdlib, but especially the internal projects that are written in Go. If Google engineers were to use it for a couple of months in real life situations that would provide good insights in how it would work in practice.
Obviously I can apply it myself as well and use it on my own code bases (and I might as well). But I'm just a single developer with a relatively small code base, so this wouldn't be as representative if a lot of Google employees were to use it.
Personally I'm still on the fence about this feature. On the one hand I do appreciate the fact that it may save me a couple of keystrokes each time. But at times I may be lazy (I'm human after all), and just nest as many try statements as I possibly can. Now I'm just going to be a bit more disciplined, were this feature present. But even if I was, there's still a chance that external libraries do overuse/abuse this keyword while my code base suffers from it (in terms of debugability, extendibility of those libraries).
@Freeaqingme Got it. We could certainly run try
over internal repos. I'm not sure we can convert and convert back - there's a significant cost involved here. Also, we could only report in aggregate (statistics) on this experiment as we wouldn't be able to report on internal details. That is the community would have no easy way to verify our claims. Finally, Google's code base may not be representative.
But thanks, point taken.
@griesemer I appreciate that it may be a costly endeavour. In which case I wouldn't do it. If it's simply a matter of applying your tryhard project than cost could be limited. But really that's something for a Googler (which I'm not) to determine.
I understand you wouldn't be able to report on individual Google projects or internal infrastructure. However, a few anecdotes could be shared, perhaps. But what I personally would find way more valuable is some Googlers reporting on how it worked out for them. Without going into specifics, statements like "I expected X but when I ran into cases like A and B I found that Y" I would find very valuable. I'd find zero need to verify those kind of reports.
Finally, Google's code base may not be representative.
It may be, it may not be. But there's a lot of people working at Google so I'd reckon most of the code base isn't based on how a single individual decided to write it, but rather be a culmination of contributions of many different contributors (employees). In that respect I expect things to be about as representative as things could get. There probably is no such thing like a 100% representative code base.
Keep it as it is, until we find a better solution, the try
is not the one we were looking for. No need to take bias actions on this, take your time Go Authors. I strongly believe, as far as I speak with gophers around the globe every day, that the majority of the Go Community don't embrace the try
proposal atm.
Introducing a new syntax means that everybody has to upgrade their Go version. I am still using Go 1.10 because my workflow is based on the fact that I can go get
things and then use them from the command line (my GOPATH
is in my PATH
). I recently had a problem when trying to go get
someone else's library which uses modules. I got an error that .../v2
was not available. This means that there is already a split in code (think Python 2 and 3). For me there is a world before Go 1.11 and after Go 1.11. This is very annoying and introducing a new syntax for something that works as well as error handling in Go is not a good trade-off at all. It introduces more fragmentation.
On 7/4/19, gonutz notifications@github.com wrote:
Introducing a new syntax means that everybody has to upgrade their Go
version. I am still using Go 1.10 because my workflow is based on the fact
that I cango get
things and then use them from the command line (my
GOPATH
is in myPATH
). I recently had a problem when trying togo get
someone else's library which uses modules. I got an error that.../v2
was
not available. This means that there is already a split in code (think
Python 2 and 3). For me there is a world before Go 1.11 and after Go 1.11.
This is very annoying and introducing a new syntax for something that works
as well as error handling in Go is not a good trade-off at all. It
introduces more fragmentation.If it's any consolation, I am in exactly the same position vis-a-vis
Go modules. I haven't found the time and opportunity to get familiar
with them, so I'm sticking with Go1.10 as well. Maybe that ought to be
a survey worth having.
Lucio.
--
You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub:
https://github.com/golang/go/issues/32825#issuecomment-508372318
--
Lucio De Re
2 Piet Retief St
Kestell (Eastern Free State)
9860 South Africa
Ph.: +27 58 653 1433
Cell: +27 83 251 5824
FAX: +27 58 653 1435
I'm a new golang developer(still learning about go). I thinks that current error handling is good because it make us manage error easily. As a android developer, I thinks try-catch
is harder to manage our error than if err != nil{ }
in golang. And I think explicit error handling always better than implicit error handling.
PS. Sorry for my language.
It is not broken....
Love the meme, @Daniel1984 :-)
Incidentally, the try
proposal does leave if err != nil
alone; it just gives you an additional option where it makes sense.
I am of the opinion that try
should not be included. On the inclusion of try
:
Pro
throws
).nil
checks.Con
try
adds a duplicate method for an existing operation.try
to return from the current function is unexpected, AKA more magic.try
in words.@griesemer that's exactly what I don't like. Things should be simple, I wouldn't want to add complexity to language just to have 2 ways of achieving same thing. There are patterns to avoid this if err != nil
verbosity. https://www.ardanlabs.com/blog/2019/07/an-open-letter-to-the-go-team-about-try.html
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?
Very much this. Explicit code is correct code. The horrors I have seen with exception handlers are enough to walk away forever from that terrible unreadable construct.
There seems to be a massive lag, with people starting to comment only
now and one could not help getting the impression that it is fresh
news to them.
That needs taking into account too. The grapevine is clearly not as
rapid as one may think.
Lucio.
And change gofmt to allow single-line if statements that simple hand-ball and error up to the
calling function. That would remove a lot of clutter.
Instead of:
err = myFunction()
if err != nil {
return err
}
Allow:
err = myFunction()
if err != nil { return err}
Incidentally, the try proposal does leave if err != nil alone; it just gives you an additional option where it makes sense.
This precise justification is how Go becomes yet another C++, C#, Scala, Kotlin, etc. "Well you don't need to use it if you don't want to" is how feature bloated languages get made.
Edit - this may have come across the wrong way. I'm not saying that try
is going to turn Go into a feature bloated language. I'm saying that the justification here is a bit flawed
@deanveloper Your have clearly example of difficult understanding error behavior with "try":
https://github.com/golang/go/issues/32437#issuecomment-498932961
Even if it's a little repetitive, I agree with OP.
In addition, imo all alternatives proposed introduce useless complexity.
It just will be cool to omit the curly brackets when you have only one line inside scope
@gonuts
This means that there is already a split in code (think Python 2 and 3). For me there is a world before Go 1.11 and after Go 1.11.
I am a long time Python programmer and the alleged "split" you mention regarding Go modules is nothing compared to the disaster of the migration from Python 2 to Python 3.
This is very annoying and introducing a new syntax for something that works as well as error handling in Go is not a good trade-off at all. It introduces more fragmentation.
try
is a built-in function in the proposal. It's fully backward compatible. If your code already uses try
as an identifier, then your identifier will shadow the try
built-in.
@pongsatornw
I thinks that current error handling is good because it make us manage error easily. As a android developer, I thinks try-catch is harder to manage our error than if err != nil{ } in golang. And I think explicit error handling always better than implicit error handling.
Have you read the proposal in full? try
is just a built-in function that helps factoring out the repetition of if err != nil { return ..., err }
. The general logic of error handling in Go stays the same. It's still explicit, errors are still values, and there is no try-catch (aka exceptions).
@kroppt
try
adds a duplicate method for an existing operation.
try
is just factoring repetitive code. It's the same with append
. We can write it ourselves each time we append elements to a slice, but it's easier to call append
.
- It adds inconsistencies to error checking.
You can manipulate a slice "manually" using [...:...]
or you can use append
, depending on what you do. There is no inconsistency. They are just different tools for different tasks. Same for errors, with a plain if err != nil { ... }
, or with try
, depending on the task at hand.
- For
try
to return from the current function is unexpected, AKA more magic.
It's unexpected because it's new. We get used to it as we use it more. And I don't think it's magic; the spec of try is very straightforward.
- Programmers with no Go experience won't understand.
- Hard to even describe what's happening at
try
in words.
Programmers with no Go experience won't understand chan
, defer
, 'go,
iota,
panic,
recover,
<-, type assertions, and many other things either without reading the documentation.
try` is easy compared to most of this.
- Doesn't change error handling.
Maybe that's a good thing, according to gophers asking to leave if err != nil
alone ;-)
@marcopeereboom
Explicit code is correct code. The horrors I have seen with exception handlers are enough to walk away forever from that terrible unreadable construct.
try
has absolutely nothing to do with exception handling. Have you read the full proposal? There is nothing comparable to exception handling as in Java or Python for example. try
is explicit. Errors must be mentioned in function signatures. Errors must be handled at the call site. There is no stack unwinding. Etc.
@gale93
It just will be cool to omit the curly brackets when you have only one line inside scope
I think most gophers had the same thought, and I read similar proposals many times in the issue tracker. But it's a much larger change than try
. It would not be reasonable to do this just for the if
statement. So you would have to change this everywhere a block is accepted. Without the {
marking the start of of the block, you have to specify a way to delimit the end of the conditional expression. You have to update the grammar, the parser, gofmt, etc. It would completely change the language surface.
@ngrilly
Moderation and keeping the language simple is important.
Some of the arguments you used could justify a great deal of change to the spec. There aren't just positive points here.
I am evaluating the decision based on whether it would help or hurt, not necessarily fully enacting a certain philosophy. You are correct that some things in the spec violate certain principles on which go
was founded, but it's all about moderation. This change does not have a positive enough impact to me to tolerate the negatives.
Hi @kroppt,
keeping the language simple is important
I agree and I think we all strive for this.
I am evaluating the decision based on whether it would help or hurt, not necessarily fully enacting a certain philosophy.
I think we are all evaluating try
based on its benefits and costs. The discussion is about defining and finding a fact-based consensus about those benefits and costs, which is what I tried to do in my previous comment.
You are correct that some things in the spec violate certain principles on which go was founded
During the last few years, I've been reading almost everything the Go team has published about Go and its design, and In don't understand what you mean. I don't think the try
proposal violates foundational principles of Go.
@ngrilly
https://talks.golang.org/2012/splash.article describes some of the concepts behind what makes go
different - clarity and simplicity, among others. I think this is the conflict some of us see with this new change. It's simpler, but it's less clear. To me it seems that the gain in simplicity is smaller than the loss of clarity. Maybe I'm wrong and am just cautious.
@kroppt I've read this article dozens of times ;-) I disagree with the idea that try is obfuscating the code. try is just a built-in function used to factor some repetitive code. We do that all the time as programmers. When we identify a repetitive pattern, we factor it out in a new function. If we didn't, we would have one long main() function with all our code inlined in it.
@ngrilly
What you are describing is in my "pro" section:
- Programmers decrease the number of keystrokes they make.
- Programmers can have a shorthand for returning from current function à la macro.
- Eyes no longer glaze over when viewing the sea of
nil
checks.
Again, I do not see the point of mentioning the universal application of a principle when we are not universally applying it here.
I disagree with the idea that try is obfuscating the code
The point of the change is to obfuscate/hide/simplify/represent code - otherwise we would see the original error check block. The question is whether it makes less clear the meaning.
I think go
originally struck a great balance of simplicity, to the point where it contributed to rather than took away from clarity. I cannot explain how they did it, but try
in my opinion does not.
I don't think we should consider verbosity a problem. Code needs to be read and understood by humans — whose time is more expensive than computers' — and _understanding_ tends to be the difficult and time-consuming part.
I find that the indentation structure of go error handling helps me follow what's going on. Every error check is explicit. Most unhandled errors are also explicit. This makes the code quick to understand, for me.
I'd also think that while if err != nil
checks can seem tedious, I needn't actually _type_ them. I just have my editor do it.
@kroppt
The point of the change is to obfuscate/hide/simplify/represent code - otherwise we would see the original error check block.
But you could use this argument for any function call! If I call strings.HasPrefix("foobar", "foo")
, does it obfuscate the code? Would you prefer writing and reading l := len("foo"); len("foobar") >= l && s[0:l] == "foo"
?
@rossmcf
Every error check is explicit.
try is still explicitly checking the error. It's the raison d'être of try.
I'd also think that while if err != nil checks can seem tedious, I needn't actually type them.
It's not tedious to type. It's tedious to read, when we have the same 3 lines everywhere. It's a repetition, and we as programmers usually tend to factor these out. Maybe try
has other drawbacks, but not this one I think.
try is still explicitly checking the error
The difference between thetry
abstraction and strings.HasPrefix is thattry
implicitly returns.
When reading go code, I know that the execution flow remains within my function until I:
- read the closing bracket of a function without return types
- read the keywords
return
,panic
- read
syscall.Exit(code)
Withtry
, I could not read code the same way. Visually scanning over lines and seeing zero "return" statements would no longer mean "either these lines all execute, or one blocks, or the program terminates."
@ngrilly You can reply to more than one person in a post FYI, 10 replies in a couple hours with 5 in a row at one point makes it difficult to follow the discussion. After reading your posts aside from some fallacies I haven't seen any new concrete arguments formed describing the benefits of try. I've seen a single benefit: preventing the typing of if err != nil
. This comes at a cost of introducing new ways to leak resources, the ability to write less concise code and worst of all allows nesting of try.
I feel the spec and the arguments formed by the proponents are misleading, it currently provides only the best-case examples without showing any of the worst-case examples. It does't evaluate or mention the negative drawbacks above or any potential mitigating factors. It does not justify why it does not limit the implementation of try to the proposed and demonstrated usage. The tool tryhard
is being used to show more compact code which subjectively provides better aesthetics to some people, without a trytoohard
tool that shows what the full capabilities of what it can do, e.g. deeply nested try statements. Finally the name itself is associated ubiquitously in the programming world with exceptions, allow it to be nestable and related to errors and place it adjacently as a builtin to an unrelated recover and panic leaving things simply looking out of place. I believe and trust in the Go authors ability to do come up with something better.
There is too much cost here to justify with a single value add I see regurgitated in replies by proponents: "I no longer have to type if err != nil
" - the thing that has been fervently defended and ingrained along with the errors are values by the entire Go community. We have coming up to a decade of code written using if err != nil
- which some of the most notable technology advancements (docker, k8s, ...) in the same time all use with great success.
In closing if err != nil
isn't a burden to hide away with a builtin, but something we should all recognize as a core ingredient to the languages success. Even if we collectively accept it is a burden, the bar to remove it should be high and uncompromising. Right now too many aspects of try are a compromise.
I have opinions about which method is easier but they are opinions. Given try is simple and the current explicit checks are simple. Great both ways are simple. The problem for me is that it increases the cognitive load of both the reader and the writer of any given code. Now both have to interpret multiple ways to do things. And the writer has to choose which way to do things and/orrisk doing things differently than the rest of the package or project they are working on. If try were replacing explicit check that would still increase cognitive load because of implicit returns as another thing to parse.
_Putting all of that aside for a moment and considering we now have two equally simple ways of handling errors we still have a problem:_ Simple is no longer easy. And that opens the door to all the things go was designed to avoid.
The bar to add something like this should be much much higher, should be experimental for a long time to prove that it is better with data from the field.
@cstockton
You can reply to more than one person in a post FYI, 10 replies in a couple hours with 5 in a row at one point makes it difficult to follow the discussion.
Ian has suggested 7 days ago to move this discussion to golang-nuts exactly for this reason (no way to reply to a specific comment, no threading), suggestion which was dismissed to make sure the discussion would be "official". We have what we asked.
@therealplato
The difference between the try abstraction and strings.HasPrefix is that try implicitly returns.
That's true. When reading a function and looking for "exit" points, we'll have to look for return, panic, log.Panic, os.Exit, log.Fatal, etc... and try. Is that such a problem? The number of exit points in a function will stay the same and will still be explicitly marked, with or without try.
read the keywords return, panic
panic is not a keyword; it's a built-in function. If we're going to criticize the proposal of the Go team, which is probably more competent about language design than any of us, then at least we should do them the favor of properly defining things. 😉
When reading a function and looking for "exit" points, we'll have to look for return, panic, log.Panic, os.Exit, log.Fatal, etc... and try. Is that such a problem?
It is a problem, because try
can appear literally anywhere that an expression can appear. Every single exit point in Go can _only_ occur as a single statement, try
is the only thing that can appear as an expression.
@ngrilly
Ian has suggested 7 days ago to move this discussion to golang-nuts exactly for this reason (no way to reply to a specific comment, no threading), suggestion which was dismissed to make sure the discussion would be "official". We have what we asked.
begin message
@ user1
reply 1
@ user2
reply 2
end message
This is what was meant.
@cstockton
I've seen a single benefit: preventing the typing of if err != nil.
try
prevents the repetitive typing and reading of if err != nil { return ..., err }
(formatted on 3 lines), not just if err != nil
.
This comes at a cost of introducing new ways to leak resources, the ability to write less concise code and worst of all allows nesting of try.
The risk of resource leaks you mentioned can be prevented with vet
and lint
.
About "less concise code", the whole point of try
is to write more concise code, so I don't understand your argument.
The risk of excessive function call nesting is not specific to try
. Any function calls can be overly nested. Code reviews and linting will help, as always.
I feel the spec and the arguments formed by the proponents are misleading, it currently provides only the best-case examples without showing any of the worst-case examples.
Maybe this feeling is mutual. We're all lovely gophers; let's not fall into value judgment ;-)
I see regurgitated in replies by proponents: "I no longer have to type if err != nil"
Again, I no longer have to type l := len("foo"); len("foobar") >= l && s[0:l] == "foo"
.
I can use strings.HasPrefix("foobar", "foo")
instead.
How is it so different with try
?
I read earlier you would accept a "restricted" try
that would be named check
and would forbid nesting.
We have coming up to a decade of code written using if err != nil - which some of the most notable technology advancements (docker, k8s, ...) in the same time all use with great success.
We also have a lot of great code written in C, C++, Java, etc. With this line of reasoning, we wouldn't have Go.
When reading discussions on the error handling in Go, I didn't feel like everyone was on the same page regarding the try
proposal, so I decided to write a blog post that demonstrates how try
can be used: https://faiface.github.io/post/how-to-use-try/
Associated discussion on Reddit: https://www.reddit.com/r/golang/comments/c9eo3g/how_to_use_try_faiface_blog/
I know this issue is against try
, but I hope my post can bring some new perspectives :)
Go is stuck and struggling between magical, or logical for the idea of simple.
Pros:
Cons:
if != nil
but you could use try
I feel like this community esp. here differs against people voted in Go Survey[1].
The voters might not choose this as a main concern rather leave for future considerations.
But it was regarded to have an impact because of its placement.
IMO, Adding language feature is old and the modern programming way is adding more feature on the editors i.e. Emmet or language snippets, code folding and coloring, refactoring and formatting, debugging and testing, suggesting solution to an error and citing from godoc or stack overflow, UI on top of the source code, and leave the source code verbose
code fold if err != nil
into a try
@ngrilly
try prevents the repetitive typing and reading of if err != nil { return ..., err } (formatted on 3 lines), not just if err != nil.
Do you believe me saying "it prevents you from typing if err != nil" meant that I had completely forgotten we also read the code we type?
The risk of resource leaks you mentioned can be prevented with vet and lint.
I linked a discussion why I feel vet and lint is not a reasonable option here.
About "less concise code", the whole point of try is to write more concise code, so I don't understand your argument.
Yes, had you read the link that "the ability to write less concise code" actually pointed to you may have understood my argument. _Note, taking a small amount of time to understand the arguments of the people you are participating in discussion with is the first step to presenting information capable of causing them to concede to your view._
The risk of excessive function call nesting is not specific to try. Any function calls can be overly nested. Code reviews and linting will help, as always.
Interesting, lets actually break this down:
1) The risk of excessive function call nesting is not specific to try.
Yes, everyone here understands how functions work.
2) Any function calls can be overly nested.
Yes, everyone here understands how functions work.
3) Code reviews and linting will help, as always.
Yes, you made the lint argument already and the "code reviews argument" is another outside language control that was made in the posts I linked to you.
Maybe this feeling is mutual. We're all lovely gophers; let's not fall into value judgment ;-)
I don't understand? The proposal has no examples of the full capability the implementation provides, only the intended usage. The tryhard
tool used to help measure the affects of the proposal uses try
in the most limited and reasonable form, this is a simple fact.
Again, I no longer have to type l := len("foo"); len("foobar") >= l && s[0:l] == "foo".
I can use strings.HasPrefix("foobar", "foo") instead.
How is it so different with try?
I do my best to extract a position from each opposing view, otherwise I can't form an argument to dismantle it. I really can't see that here, I'm sorry. I'm going to interpret this the only way I can make sense of it: literally.
How is it (
strings.HasPrefix
) so different withtry
?
func HasPrefix
func HasPrefix(s, prefix string) bool
HasPrefix tests whether the string s begins with prefix.
func
try
is a new function-like built-in called try with signature (pseudo-code)func try(expr) (T1, T2, … Tn)
where expr stands for an incoming argument expression (usually a function call) producing n+1 result values of types T1, T2, ... Tn, and error for the last value. If expr evaluates to a single value (n is 0), that value must be of type error and try doesn't return a result. Calling try with an expression that does not produce a last value of type error leads to a compile-time error.
The try built-in may only be used inside a function with at least one result parameter where the last result is of type error. Calling try in a different context leads to a compile-time error.
Invoking try with a function call f() as in (pseudo-code)
x1, x2, … xn = try(f()) turns into the following (in-lined) code: t1, … tn, te := f() // t1, … tn, te are local (invisible) temporaries if te != nil { err = te // assign te to the error result parameter return // return from enclosing function } x1, … xn = t1, … tn // assignment only if there was no error
In other words, if the last value produced by "expr", of type error, is nil, try simply returns the first n values, with the final nil error stripped. If the last value produced by "expr" is not nil, the enclosing function’s error result variable (called err in the pseudo-code above, but it may have any other name or be unnamed) is set to that non-nil error value and the enclosing function returns. If the enclosing function declares other named result parameters, those result parameters keep whatever value they have. If the function declares other unnamed result parameters, they assume their corresponding zero values (which is the same as keeping the value they already have).
If try happens to be used in a multiple assignment as in this illustration, and a non-nil error is detected, the assignment (to the user-defined variables) is not executed and none of the variables on the left-hand side of the assignment are changed. That is, try behaves like a function call: its results are only available if try returns to the actual call site (as opposed to returning from the enclosing function). As a consequence, if the variables on the left-hand side are named result parameters, using try will lead to a different result than typical code found today. For instance, if a, b, and err are all named result parameters of the enclosing function, this code
a, b, err = f() if err != nil { return }
will always set a, b, and err, independently of whether f() returned an error or not. In contrast
a, b = try(f())
will leave a and b unchanged in case of an error. While this is a subtle difference, we believe cases like these are rare. If current behavior is expected, keep the if statement.
They are different in that the entire text found in the description of try is not present in strings.HasPrefix. A better question would be how are the similar, to which I would reply they both share some aspects of Calls and nothing else.
I read earlier you would accept a "restricted" try that would be named check and would forbid nesting.
Glad you read my central argument against try: the implementation is not restrictive enough. I believe that either the implementation should match all the proposals usage examples that are concise and easy to read.
_Or_ the proposal should contain examples that match the implementation so that all people considering it can be exposed to what will inevitably appear in Go code. Along with all the corner cases that we may face when troubleshooting less than ideally written software, which occurs in any language / environment. It should answer questions like what stack traces will look like with multiple nesting levels, are the locations of the errors easily recognizable? What about method values, anonymous function literals? What type of stack trace do the below produce if the line containing the calls to fn() fail?
fn := func(n int) (int, error) { ... }
return try(func() (int, error) {
mu.Lock()
defer mu.Unlock()
return try(try(fn(111111)) + try(fn(101010)) + try(func() (int, error) {
// yea...
})(2))
}(try(fn(1)))
I am well aware there will be lots of reasonable code written, but we are now providing a tool that has never existed before: the ability to potentially write code without clear control flow. So I want to justify why we even allow it in the first place, I never want my time wasted debugging this kind of code. Because I know I will, experience has taught me that someone will do it if you allow them. That someone is often an uninformed me.
Go provides the least possible ways for other developers and I to waste each-others time by limiting us to using the same mundane constructs. I don't want to lose that without an overwhelming benefit. I don't believe "because try is implemented as a function" to be an overwhelming benefit. Can you provide a reason why it is?
Don't waste time on this error handling issue, give us generics and we will create something like Rust's Result.
Go is stuck and struggling between magical, or logical for the idea of simple.
Pros:
- Reduce boilerplate
- Simple
- Existing Pattern among other languages
- Optional
Cons:
- Learning Curve
- Underlying magic
- New kinds of bugs
- Go is opinionated, and pragmatic with
if != nil
but you could usetry
I feel like this community esp. here differs against people voted in Go Survey[1].
The voters might not choose this as a main concern rather leave for future considerations.
But it was regarded to have an impact because of its placement.IMO, Adding language feature is old and the modern programming way is adding more feature on the editors i.e. Emmet or language snippets, code folding and coloring, refactoring and formatting, debugging and testing, suggesting solution to an error and citing from godoc or stack overflow, UI on top of the source code, and leave the source code verbose
code foldif err != nil
into atry
I was one who voted for stricter error handling without a possibility to forget to process an error. Not for try.
We'll need a lot more than generics to remake anything remotely like Rust's Result
type. Even _if_ the Result
type could be made solely with generics, beginner programmers would then need to know generics before they could even handle an error properly, or return an error from a function "the Result
way"
@deanveloper, my point is: I have a lot more to benefit from generics than from "syntax change" and I believe it is also true for the community.
@txgruppi I can agree that generics should have higher priority. I was just trying to say that I don't think that generics will be a good substitute for error handling.
@deanveloper, in my opinion this error handling issue is just cosmetics, people are spending time discussing someting thst is stable and works well just because you havr to type a few extra code. Just learn how to write better code and resolve this with design changes.
Before someone says Generics is just as simple to fix with better code: compile time errors...
can it be solved with a snippet or a keyboard macro? then it is not an issue.
@txgruppi
Just learn how to write better code and resolve this with design changes.
70% of all error-handling code in the standard library is currently suitable for try
as Robert Griesemer found out using his tryhard
tool. More would be eligible with changes to the code, such as using the (not yet existing) fmt.HandleErrorf
function. I hope you don't want to call the standard library a bad code.
can it be solved with a snippet or a keyboard macro? then it is not an issue.
It's also about reading the code. That's why we don't like thing.Thing thing = new thing.Thing(thing.THING);
@faiface, is 'if err != nil' getting on the way of developing quality software? I dont think so.
Is lack of Generics getting on the way of developing quality software? Yes, it is.
The way I see it is: I don't have enough nowledge to implement generics so I need someone to implement it, but this error handling thing is just a waste of time for those minds that can make generics a reality. I'm not against this error handling because it is a bad thing, I'm against it because there are more important things to solve.
@faiface the standard library is not a good representation of real Go code. This is because it's much more likely for the standard library to simply pass-up errors without adding context, for instance io/ioutil
never really needs to decorate errors, it can simply pass up the error that occurred in io
. Robert Griesemer also admitted that the stdlib isn't exactly the best representative of real life Go code, however I'm on mobile right now and don't want to go looking for the comment. I'm pretty sure it was relatively close to his original tryhard post though.
@deanveloper @faiface When run against the Go Corpus:
--- stats ---
401679 (100.0% of 401679) func declarations
97496 ( 24.3% of 401679) func declarations returning an error
991348 (100.0% of 991348) statements
217490 ( 21.9% of 991348) if statements
88891 ( 40.9% of 217490) if <err> != nil statements
485 ( 0.5% of 88891) <err> name is different from "err" (-l flag lists details)
59500 ( 66.9% of 88891) return ..., <err> blocks in if <err> != nil statements
29391 ( 33.1% of 88891) complex error handler in if <err> != nil statements; cannot use try (-l flag lists details)
596 ( 0.7% of 88891) non-empty else blocks in if <err> != nil statements; cannot use try (-l flag lists details)
52810 ( 59.4% of 88891) try candidates (-l flag lists details)
So, in real-life code, 40% of if-statements are written for error-checking, and try
can eliminate 59% of them out of the box.
I agree. I'm fine with if err != nil. It's simple and clean for functions that return single error values. I'm also fond of the errors package and its cause / wrap functions when context of the error matters. Using custom errors with a code property (as far as I know) requires you to either do a type assertion or to use something in place of the standard error interface altogether. Either way, I've never found myself reading or writing Go code and feeling any sort of annoyance with how error handling currently works. The annoyances I have ran into are cases where multiple errors can occur as a result of processing a collection of items. However, this is design problem and not a language problem.
Note that when I say "when context of the error matters" I'm referring to a situation where maybe a network hiccup occurred and so I want to retry, or maybe the results of a "find" type call returned no results because there were actually none, or my twitchy finger added a random 's' to my SQL query which is making it blow up (this happens to me often...I should probably get checked for nerve damage).
On 7/5/19, Nicolas Grilly notifications@github.com wrote:
@kroppt I've read dozens of times ;-) I disagree with the idea that try is
obfuscating the code. try is just a built-in function used to factor some
repetitive code. We do that all the time as programmers. When we identify a
repetitive pattern, we factor it out in a new function. If we didn't, we
would have one long main() function with all our code inlined in it.I'm tempted to call you disingenuous, but I respect Ian Lance Taylor's
superhuman efforts to keep the discussion polite and I really can't
see what anyone would gain by intentionally lying in this forum.
That said, "When we identify a repetitive pattern, we factor it out in
a new function." Sure, but not by providing a conflicting construct
that, at this late stage of Go's development, comes long with two
hidden features: the first is treating functions whose "return
argument list ends in an error
value" as special (or everything else
as a semantic error) and the second it providing a hidden control flow
detour that is analogous but not entirely identical to a "return"
statement.
Never mind the flaming hoops that using "defer" introduces to deal
with more arcane uses of "try - the pseudo function". Somebody
elsewhere said, roughly "I don't want to come across try
in the code
I read". I feel the same and that should not be swept under the
carpet.
I have stated that it is the "return" aspect of "error return" that
needs to be dealt with, and the "on err" proposal comes closest to
that principle, but also bends the rules somewhat. So does my own
"fail" suggestion (it moves the last argument to be the first, that's
making me unhappy).
More deeply, what no language with the possible exception of SNOBOL,
that I am familiar with has, taken the leap that Rob Pike described as
"errors are values" to the degree that Go has, but something got lost
in the process: an error "condition" is "not" a value. Successful
completion of a function is a special case and so is each possible
failure.
Each (and that applies to successful completion, of which there may be
more than one, as well) needs to be treated on its merit, but we
insist that the invoked function must tell us its opinion regarding
the quality of completion in an abbreviated form, something that has
been done forever and Rob has shown to be a misconception.
To illustrate the point, consider the return values from a Reader:
io.EOF is a special case that is sometimes a success and sometimes a
failure, but by Go standards is proudly an error ("io.Err != nil").
Are we going to have some way to abbreviate that too? Almost certainly
not, because we are quite used to "forgive" its "wrongness".
I have long waited for a loop exit to convey a similar "status" or
condition code (a search may end with a found value or a failure, how
do you tell which, if you wish to do different things? You add a test,
where the knowledge is already in place - same issue, different
context).
These are real enhancements to traditional languages: reducing boiler
plate is absurd, by comparison.
And, the comparison with the ternary operator is equally valid: if ?:
is not allowed "lest it be abused", then try must not be allowed,
either, at least on those grounds.
Frankly, try is a lark. It makes Go more appealing to the wrong
audience. Beside the danger in doing that - who wants the wrong people
to join our community? - there is the issue of precedent and the issue
of unintended consequences. I'd say "scrap it" and let's accept that
we have not yet arrived at a point where backwards compatibility can
be flouted, so error handling must remain verbose.
We have panic/recover and it needs to be promoted from third-rate
citizen to the willing helper it can be, with all the explanations
that make newbies (and me, I admit to being scared of it) more
confident using it.
The "defer" construct within error handling (which I have adopted -
without realising its import elsewhere - consistently in finalising
SQL transactions: tx.Rollback/tx.Commit) has been a revelation for me.
There may be more principles that can be learnt "within" the scope of
what Go already offers: let's stay within that box for now.
One such thing, off the cuff, would be to pass to an error reporting
function a list of "error.Methods" to be executed under conventional
conditions (io.EOF, sql.ErrNoRows), instead of reporting the outcome
as black-and-white. But I'm unschooled in such matters, my suggestions
are too naive, let others (Roger, are you listening?) nurture similar
ideas to fruition.
Lucio.
"It's not tedious to type. It's tedious to read, when we have the same
3 lines everywhere. It's a repetition, and we as programmers usually
tend to factor these out. Maybe try has other drawbacks, but not this
one I think."
Again, disingenuous or at minimum, rationalising. I grant that those
three lines are read many more times than they are written, but the
pain Griesemer aimed to alleviate is in the writing not in the reading
But "err != nil" is a very familiar marker and when "reading" - as
opposed to "searching" using an editor, that pattern is both easy to
spot and easy to cancel out. Factoring it out is not in the same
league at all. And the price is wrong.
"We as programmers" tend to factor out more complex patterns first,
even if they occur rarely. We also know that "if err != nil { return
err }" is compiled into a very simple sequence of instructions, if
anyone doesn't, let them raise their hand here. Can one be equally
confident that it will happen with "try - the function"?
Lucio.
@lootch Hey man, it's really unproductive to call people disingenuous, I'm pretty sure they are not as the statements you marked as such are pretty reasonable.
We do factor out repetitive patterns as programmers, that's absolutely true.
A lot of repetitive code does slow down reading, so I don't see how that's disingenuous either.
Your counterarguments to these are basically "come on man, it's not such a big deal". Well, for many people it is.
who wants the wrong people to join our community?
This is super arrogant gate keeping. Also, tryhard
tool has revealed that try
is directly applicable in much of today's Go codebases. It's can be directly applied to 70% of error handling code in the standard library. With changes to the code (to use error decoration using defer
, etc), I believe it will be applicable to more than 80% of error handling in all Go code.
Agreed, I am overstepping the mark, here. I apologise.
I guess some of us are getting hot under the collar as this discussion
is going around in circles.
On 7/7/19, Michal Štrba notifications@github.com wrote:
@lootch Hey man, it's really unproductive to call people disingenuous, I'm
pretty sure they are not as the statements you marked as such are pretty
reasonable.We do factor out repetitive patterns as programmers, that's absolutely
true.A lot of repetitive code does slow down reading, so I don't see how that's
disingenuous either.Your counterarguments to these are basically "come on man, it's not such a
big deal". Well, for many people it is.who wants the wrong people to join our community?
This is super arrogant gate keeping. Also,
tryhard
tool has revealed that
try
is directly applicable in much of today's Go codebases. It's can be
directly applied to 70% of error handling code in the standard library. With
changes to the code (to use error decoration usingdefer
, etc), I believe
it will be applicable to more than 80% of error handling in all Go code.--
You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub:
https://github.com/golang/go/issues/32825#issuecomment-508971768
--
Lucio De Re
2 Piet Retief St
Kestell (Eastern Free State)
9860 South Africa
Ph.: +27 58 653 1433
Cell: +27 83 251 5824
FAX: +27 58 653 1435
@lootch Props on being self-conscious! I can understand the frustration of seeing the discussion Go in circles.
I also see it the similar way and I'm on the other side.
Perhaps both sides are just failing to understand each other. Have you read my blog post called How to use 'try'? where I attempt to show how using 'try' would look like in practice, doing my best to stay unbiased?
On 7/7/19, Michal Štrba notifications@github.com wrote:
[ ... ]
Perhaps both sides are just failing to understand the other side. Have you
read my blog post called How to use
'try'? where I attempt to
show how using 'try' would look like in practice, doing my best to stay
unbiased?I confess I did not, I'm wishing fervently that I never have to :-)
Have you considered the aspects I think Stockton raised, where only
the upsides of try are displayed and he asks that the soft underbelly
be revealed, too? I fear I agree with him and - no offence intended -
that your blog may suffer from the same shortcomings.
If not, then please prod me, good reading matter has a special place
in my life :-)
Lucio.
@lootch I did my best to show as many aspects of 'try' as possible (i.e. both when it applies and when it does not) in short amount of code and make it as unbiased and realistic as I can. But of course, don't take my word for it :)
This was the top-upvoted comment on the associated Reddit discussion:
This is a useful unbiased hypothetical example. Thanks for adding something constructive to the conversation that's not just "it sucks".
@lootch I did my best to show as many aspects of 'try' as possible (i.e. both when it applies and when it does not) in short amount of code and make it as unbiased and realistic as I can. But of course, don't take my word for it :)
This was the top-upvoted comment on the associated Reddit discussion:
This is a useful unbiased hypothetical example. Thanks for adding something constructive to the conversation that's not just "it sucks".
Function with file path as an argument? This alone would be a reason this code would not pass my review. What if some fields are missing? Reordered?
@sirkon For the purposes of not being too long, the example is simplified, of course. The changes that would be required to fix the problems you raised don't affect the error handling practices, though, which is all that matters there.
The changes that would be required to fix the problems you raised don't affect the error handling practices
Because you said so?
go
resp := Respondent{
Name: name,
Gender: try(parseField(s, &line, "gender")),
OS: try(parseField(s, &line, "os")),
Lang: try(parseField(s, &line, "lang")),
}
PS Had a look at your repos. Do you realize Go is a bad tool for your tasks? You should understand in real Go's application practice the first who will see logs will be operation engineers, not developers. Proper error message may help them to solve a problem themselves.
@sirkon Come on, don't do flamewars.
Do you realize this will produce poor error messages?
They are entirely adequate to the model. The format is expected to contain all the fields and in order. The error message tells it very clearly.
If you wish to contest the quality of the code, why don't you rewrite it to your standards of quality? If you do so, I'll try and do my best to rewrite your code to use try
.
PS Had a look at your repos. Do you realize Go is a bad tool for your tasks?
Do you suggest another for my tasks? I've used quite a few. Btw, this is quite off-topic.
@faiface
Do you suggest another for my tasks? I've used quite a few. Btw, this is quite off-topic.
Rust? C++?
@sirkon
Rust? C++?
There we go. I've used both of them before settling with Go. I've never looked back.
@sirkon One of the huge flaws of try
is that it discourages error decoration. The programmer in this case was showing possible applications of try
, so of course there won't be much error decoration going on.
Also, discrediting people based on the projects that they've worked on is entirely off-topic and uncalled for. You have been quite rude with your past few comments, and I want you to at least be aware of that.
@deanveloper Thanks for the comment!
Btw
The programmer in this case was showing possible applications of try, so of course there won't be much error decoration going on.
In case you're referring to my blog, there actually is quite a lot of error decoration going on, just not the exact same way as @sirkon would do it. Here are some error messages from the program that uses try
:
parse respondnts.txt: open respondnts.txt: no such file or directory
parse respondents.txt: line 12: parse field gender: expected "gender:"
parse respondents.txt: line 9: expected empty line
parse respondents.txt: line 4: parse field lang: EOF
@faiface My mistake, I should have been more specific. try
discourages error decoration for when you want multiple error messages within the same function. It was possible to do this with check/handle
draft and with the "named handler" counterproposals. It would have been very useful in the specific instance pointed out (where you were using try
while initializing a struct) to be able to add decoration around each message, but unfortunately the try proposal makes that a bit hard to do without writing your own function.
Check/handle wouldn't have helped as much in your specific case though. But the proposed idea of catch
and other counterproposals to try
would have been able to handle the errors as as add additional decoration.
@deanveloper Well, most of the time you need to use the same decoration for all errors within a function because the sub-functions should be expected to provide their own context. However, when you do have to decorate things differently in a single function, there is still an easy solution with try:
..., err := functionThatCanFail(...)
try(errors.Wrapf(err, ...))
Or simply split the big function into multiple small ones.
@faiface in my eyes at that point one should just use if err != nil
, but I guess it's a matter of preference.
However sometimes (like in the struct-initializing case) it isn't a good idea to split up into multiple functions. I'm getting a little nitpicky though I guess.
I'm actually not super against try
, but I'm not exactly a big supporter either. I think that there's another better solution out there.
@deanveloper
However sometimes (like in the struct-initializing case) it isn't a good idea to split up into multiple functions.
True, but it also isn't needed to decorate them differently, because all the required specific decoration comes from parseField
.
I think that there's another better solution out there.
That's quite possible! If I see a better solution, I'll drop try
in a minute :)
most of the time you need to use the same decoration for all errors within a function because the sub-functions should be expected to provide their own context
@faiface I strongly disagree with this statement. Every function is a sub-function of another one on the call stack. It means it has the same responsibilities in error handling flow (that is provide error context to the upper scope).
Imagine a function that appends two chunks of data to a single file. How would you distinguish which one of those appends failed if you barely return a 'could not write to file' statement?
We are all lazy creatures. Me too I would prefer doing something once for all if I could. And yes, when I started my adventure with Go I considered error handling to be a bit cumbersome. After a few years practice though my view turned 180 degrees. I believe current error handling in Go promotes responsible programming and good design. IMHO it would be a huge fail to add another mechanism that undermines this approach.
@mklimuk A key part of my comment is "most of the time". The example you provided is probably best handled by if err != nil
. As noted many times, try
isn't designed to handle all situations, just the most common ones.
And evidence shows that it does do so, as 70% of all error handling code in the standard library can use try
out-of-the-box and the same goes for 59% of all error handling code in the wild.
@faiface well the fact that try
can replace explicit error handling does not mean it should. In my case returning an error without adding context to it is not 'the most common situation'. It is the opposite :)
People upvoting this thread are just concerned that this new statement will spoil the whole effort behind original Go design (simplicity, clarity etc.) for the sake of making Go code less verbose.
Sure, but I hope you understand that try
isn't for returning an error without a context. In fact, the most common case of adding context (one context per function) is vastly simplified by try
:
func doSomething() (err error) {
defer fmt.HandleErrorf(&err, "doing something")
x := try(oneThing())
try(anotherThing(x))
// ...
}
The thing to realize is that most of the time, oneThing()
and anotherThing()
will return a sufficient context on their own, so wrapping it in a simple "doing something: ..."
is completely sufficient.
As a side note I think we could use some conventions on _who_ does the decoration. In the stdlib some functions do this, ex copy: x to y
or similar, I've personally been leaving the decoration up to the caller, as it has the arguments.
For example if I had a Copy()
I'd do something like return errors.Wrap(err, "writing")
and the caller using Copy()
would wrap with errors.Wrapf(err, "copying from %v to %v", src, dst)
or similar.
These two don't mix and match too well, and sometimes can end up with duplicate strings, is it best to just say the stdlib style is idiomatic? I don't recall them all behaving like this though. I think that's the only way @faiface's example there would be sufficient. Maybe I've been inverting the problem though, to me it just feels cleaner to do less and leave decisions to the caller, especially "just in case" they omit contextual information.
I've personally been leaving the decoration up to the caller, as it has the arguments.
Yes. For example, consider a function which parses the JSON body out of an HTTP request, checking the headers and so on. If it gets fed syntactically invalid JSON, its responsibility — all it can do, really — is to report back the error. The _caller_ knows what part of the API was attempting to be called, and needs to decorate the error accordingly before in turn either passing it up the chain or issuing an HTTP error.
If your functions are truly factored out general purpose code that could be used multiple places, they won't have the information needed to decorate the error. Contrariwise if they do have all the context, they're probably not really functions that make sense as standalone functions, you're just creating functions to break the code up and make it look better organized than it actually is.
@lpar Mind giving a specific example of this?
I already gave a specific example? Consider if your parseJSON function actually knew the context and was able to decorate which API endpoint and activity flow it was parsing body for. That would suggest either that it was specific to that endpoint, or that you were passing in the information just so that you could use it to wrap errors.
@lpar Okay, so that's another example where if err != nil
will remain in use. Or you split your logic into multiple functions.
But understand that giving an example where try
isn't appropriate isn't an argument against try
. try
isn't meant to replace all error handling, just the most common cases.
@abejide001 the try
proposal is not the traditional "try/catch" from many other languages, it's much more similar to the try!
macro in Rust. Good meme though lol
Whoops - posted to the wrong issue. Moved to https://github.com/golang/go/issues/32437#issuecomment-509024693.
I recently posted a #32968 proposal that builds on my disagreement with the dangerous ability to nest that the try
macro possess. While I hope it lacks serious flaws, as an author I am not the right person to see any. So I would like to ask my _do not try_ camp (you :) to see, assess and comment on it.
Excerpt:
check
macro is not a one-liner: it helps the most where many repetitiveIt is a built-in, it does not nest in a single line, it allows for way more flows than try
and has no expectations about the shape of a code within. It does not encourage naked returns.
// built-in 'check' macro signature:
func check(Condition bool) {}
check(err != nil) // explicit catch: label.
{
ucred, err := getUserCredentials(user)
remote, err := connectToApi(remoteUri)
err, session, usertoken := remote.Auth(user, ucred)
udata, err := session.getCalendar(usertoken)
catch: // sad path
ucred.Clear() // cleanup passwords
remote.Close() // do not leak sockets
return nil, 0, err // dress before leaving
}
// happy path
// implicit catch: label is above last statement
check(x < 4)
{
x, y = transformA(x, z)
y, z = transformB(x, y)
x, y = transformC(y, z)
break // if x was < 4 after any of above
}
But understand that giving an example where
try
isn't appropriate isn't an argument againsttry
.try
isn't meant to replace all error handling, just the most common cases.
And as per the statistics I posted earlier, they aren't the most common cases in my code. The most common cases in my code are errors being wrapped before return. So try
would only be appropriate for a single-digit percentage of my error returns at best (*), which is why I think we need something better.
(*) And in fact, I'm inclined to think that the naked err
return instances are likely to be errors that should be fixed.
Totaly agree, leave "if err != nil" alone.
@abejide001 the
try
proposal is not the traditional "try/catch" from many other languages, it's much more similar to thetry!
macro in Rust. Good meme though lol
This alone is a concern to me, Go is already a strange language to newcomers, and now we must explain why try
has bespoke logic. FWIW, I don't think saying "Rust did it" is a sound reason to justify adding anything into a language--it's just not well-known.
@as I was not saying that to justify the feature, I was just saying it to clarify what the feature did. I'm quite in the middle with the try
proposal.
Not that this makes much of a difference, but it's also a feature in Swift as well, although with a keyword instead of a macro.
It seems there is some confusion as to what exactly try is trying to achieve. IMHO, the problem isn't writing multiple if blocks that check for errors. You write those once and you are done. The problem is reading code that has multiple of those blocks. We do a lot more reading than writing. And these blocks obfuscate the actual code because they intertwine with it. Worse yet, a lot of the time they are almost exactly the same, with only a minor string difference somewhere within that if block.
I personally preferred the old check - handle draft, but this at least does a good job separating error and business paths. And we might finally be able to have a single function scope context as opposed to for each call, which currently has a good chance of repeating the same thing as the parent error.
@icholy wrote:
There was overwhelming community feedback requesting more streamlined error handling (from the annual survey). The Go Team is now addressing that issue.
I just looked up the survey here: https://blog.golang.org/survey2018-results
Apparently the question was: "What is the biggest challenge you personally face using Go today?" with possible answer "Error handling".
I seriously wonder how based on that question+answer it was deduced that a more brief syntax was required. I might have also answered 'error handling', but by no means I'd have wanted to see another syntax. If I'd checked this option in the survey, I'd have thought of better allowing to wrap errors, provide them with stack traces, etc.
My suggestion would be to step back from all of the error handling proposals (effectively what @miekg was suggesting). And first determine what it actually is that the community wants, document that. Then find out why that's what they want. And only afterwards start looking at ways to achieve that.
I've just been going through the try proposal, but unless I'm missing something it neglects to say _why_ it's being proposed, other than "to eliminate the boilerplate if statements [...}". But there's no mention on why elimination of those boilerplate if statements is necessary.
I definitely agree with the above. Let's see if the new error values changes help aid the error handling complaints that people have with Go. Then we can see if a more brief syntax is required.
People here are arguing against try
because they feel that all returned errors should be annotated. The reality is that, in the current corpus of code (including the standard library), a high percentage of error checks have ~bare~ un-annotated error returns and would benefit from try
. Your belief of how code SHOULD be has nothing to do with the way code IS. Spare me your dogma.
@icholy Ignoring errors indicates that the dev doesn't care about that error. The error is insignificant or is believed by the caller to be impossible. If that is the case then "try" is just as pointless, the caller simply wouldn't wrap the function in a "try".
My suggestion would be to step back from all of the error handling proposals (effectively what @miekg was suggesting). And first determine what it actually is that the community wants, document that. Then find out why that's what they want. And only afterwards start looking at ways to achieve that.
I agree strongly with this. I see a lot of basic disagreement on what functionality any improvement to Go error handling should even support. Every different chunk of functionality people mention is triggering bikeshedding over its naming and syntax, so the discussion is going nowhere.
I'd like to know in more detail what it is that the broader Go community actually wants from any proposed new error handling feature.
I've put together a survey listing a bunch of different features, pieces of error handling functionality I've seen people propose. I've carefully _omitted_ any proposed naming or syntax, and of course tried to make the survey neutral rather than favoring my own opinions.
If people would like to participate, here's the link, shortened for sharing:
https://forms.gle/gaCBgxKRE4RMCz7c7
Everyone who participates should be able to see the summary results. Then perhaps once we have a better idea what people actually want, we'll be able to have an intelligent discussion about whether the try proposal provides those things. (And then maybe even go on to discussing syntax.)
@lane-c-wagner are you trying to saying that returning an un-annotated error it is the same as not returning it at all? edit: fixed previous comment
@icholy Ah I misunderstood. When you said "bare" I thought you meant "_" ignored errors.
This proposal argues that no action should be a valid action. This change affects all users of the language because they read the code. As such, a survey identifying the biggest hurdle still needs to ask the community whether this hurdle is worth fixing. This proposal is the closest evaluation of such a question.
Please stop saying "that everybody is free to ignore" try
. We read code written by others.
@tv42 I don't know if you are addressing me here, but I've said that as well, and you have a point. Guilty as charged. I will try to be more careful with generalizations such as that. Thanks.
@griesemer you survey was heavily lacking. I voted for error handling, but the issue I meant was full type safety, not verbosity. You better make another one about errors only.
And I still want sum types.
This is a proposal about the way gofmt currently formats if err != nil
(This is not an opinion about the try() proposal.)
When an if statement returns a one-line not-nil error value, such as:
err := myFunc()
if err != nil {
return err
}
gofmt could relax its own if-statement rule and format it on one line like this:
err := myFunc()
if err != nil { return err }
Three lines of error handling code becomes just one line. Less clutter. Easier to follow program flow.
There will need to be some judgement about where to draw the line (pun acknowledged) with this
gofmt rule change. It might include some decoration, such as:
err := myFunc()
if err != nil { return fmt.Errorf("myFunc() blew up! %v", err }
But elaborate multi-line error handling should remain as it is: multi-line and clear and explicit.
The _try_ proposal has been withdrawn: https://github.com/golang/go/issues/32437#issuecomment-512035919
Generics anyone?
This is a proposal about the way gofmt currently formats if err != nil
I have tried that, imho the code is even more unreadable that way than with multi line formatting. try is much better than that solution.
IMO the problem here is rather not how the error handling is performed, but whether it is ignored. Wouldn't it be possible to leave the if err != nil
syntax as is, but restrict the ignorance of the Error
returns? Like make it a compiler warning/error with deseverity option for the legacy code.
IMO the problem here is rather not how the error handling is performed, but whether it is ignored. Wouldn't it be possible to leave the
if err != nil
syntax as is, but restrict the ignorance of theError
returns? Like make it a compiler warning/error with deseverity option for the legacy code.
Many people want a linter showing ignored errors.
I'd prefer making this a hard error, but looking at the tons of already written legacy, linter is fair as well.
i find https://github.com/kisielk/errcheck valuable for telling me about unhandled errors @plyhun @sorenvonsarvort
As seen in the discussion on #32437, this proposal has in effect been accepted for now. Closing. If the issue arises again, a new proposal can be opened.
I'm starting to think that one of the reasons that a lot of the proposals feel like they don't quite fit right is because they're actually trying to address two different problems at the same time. On the one hand, it's true that having err != nil
blocks after nearly every function call can break up the flow of the code in a weird way, although it certainly has its upsides, but I think that that's only half of the problem. The other issue is that handling multiple returns, regardless of whether there were errors involved or not, can be quite clunky.
Multiple return functions feel very, very different from single return functions, despite the seemingly small difference between the two. It's kind of like if there were extra restrictions on calling functions that take more than one argument. It feels very odd to deal with sometimes. When you call a function with multiple return values, you almost always need to do so on its own line, and it, combined with :=
, is often the main source of the various variable shadowing problems that have been discussed elsewhere. You can't chain method calls onto them, you can't assign from them directly to a struct field and a new variable on the same line, and so on.
I don't know. Maybe it's just me. But I've used Go for nearly 10 years now and calling functions with multiple returns still feels kind of awkward to me sometimes.
Thank you!
There is one actually issue with if err != nil
, the scope of err
can live longer than it should. When you inline the if
it solves the issue, but not all case can be inlined.
if err := foo(); err != nil {
if _, err := bar(); err != nil {
md5-6a135eb952fe7b24b3389cb16d3244a1
a, err := bar()
if err != nil {
md5-d52f811d3e31bb368bd8045cfb2e93b4
var err error
baz.A, err = bar()
if err != nil {
The err
variable should not exists in the function scope after the if err != nil {}
block completes. Here is my proposal that builds off of the try()
proposal to fix the issue https://github.com/golang/go/issues/33161. I would love some constructive feedback.
The err variable should not exists in the function scope after the if err != nil {} block completes.
why "should" it not exist after the if block completes? The compiler can optimize for it (if it'd deem that necessary), and there's no mental load when the err := stmt()\nif err != nil {} block completes because these almost always go together.
I haven't yet looked at your proposal in depth (though kudo's for going through the effort of writing one!). However, as I also outlined in my comment above, I think more research is required into any perceived problems, before we dig into any proposals to resolve them.
@Freeaqingme errors should not exist after the if err != nil
block completes, mostly because we already act like it doesn't.
In the CopyFile example, there is r, err := os.Open(src)
followed by w, err := os.Create(dst)
. The second err
is shadowing the first one. Shadowing variables is usually frowned upon.
There are also other oddities. If I have err := foo()
and later something like bar.V, err = baz()
, if the code is refactored and I no longer need foo() I would need to add var err error
before the baz
line. . I don't think that refactoring a different location in a function should affect other places like that.
Technically in
r, err := os.Open(src)
if err != nil {
return ...
}
w, err := os.Create(dst)
the second instance of err
does not shadow the first instance. They are actually the same variable. See the discussion of redeclaring variables at https://golang.org/ref/spec#Short_variable_declarations.
func doSomeThing() {
r, err := os.Open(filename)
panic(fmt.Errorf(err, "failed to open file: %s", filename)) //
It's panic here.
}
On Thu, Oct 10, 2019 at 11:24 AM clearcode notifications@github.com wrote:
I think we can add a buildin function:
assert()
example:
func doSomeThing() error {
r, err := os.Open(filename) assert(err, "failed to open file: %s", filename) // in this step, just return the error
resp,err := http.Get(someURL)
assert(err, "request failed")}
and another function that donot return an error:
func doSomeThing() {
r, err := os.Open(filename)
assert(err, "failed to open file: %s", filename) // It's panic here.}
so assert(error, args ...interface{}) is better than: if err != nil ; {
return err }—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/32825?email_source=notifications&email_token=AGUV7XQ5HO7GL3YP72R7BV3QN2N55A5CNFSM4H4DL33KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEA2KWUI#issuecomment-540322641,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AGUV7XS4JMK44QHIIR3RSGTQN2N55ANCNFSM4H4DL33A
.
The takeaway is that I want to see the actual error returned in the current
function in the current line.
On Fri, Oct 11, 2019 at 9:55 AM Aaaa Einai aaaaeinai@gmail.com wrote:
func doSomeThing() {
r, err := os.Open(filename)
panic(fmt.Errorf(err, "failed to open file: %s", filename)) // It's panic here.}
On Thu, Oct 10, 2019 at 11:24 AM clearcode notifications@github.com
wrote:I think we can add a buildin function:
assert()
example:
func doSomeThing() error {
r, err := os.Open(filename) assert(err, "failed to open file: %s", filename) // in this step, just return the error
resp,err := http.Get(someURL)
assert(err, "request failed")}
and another function that donot return an error:
func doSomeThing() {
r, err := os.Open(filename)
assert(err, "failed to open file: %s", filename) // It's panic here.}
so assert(error, args ...interface{}) is better than: if err != nil ; {
return err }—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/32825?email_source=notifications&email_token=AGUV7XQ5HO7GL3YP72R7BV3QN2N55A5CNFSM4H4DL33KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEA2KWUI#issuecomment-540322641,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AGUV7XS4JMK44QHIIR3RSGTQN2N55ANCNFSM4H4DL33A
.
Frankly, I don't want an implicit return that
try
provides. If we had generics, I would much prefer a solution that used monad-ish behaviour instead.type Result<T> interface { Expect(err error) T OrElse(defaultValue T) T } func From<T>(value T, err error) Result<T> { ... }
To me, this is a lot cleaner than the builtin currently being proposed, although further changes would be required to the above since you'd have a proliferation of methods that returned (value, error) and Result
It's so similar Rust‘s Ok and Err.
I think if err != nil {}
maybe better one bit.
@Yanwenjiepy that's intentional, I'm big fan of Rust's Result
type.
I'm less than 10 minutes into learning Go. The very first thing I noticed in the code I was looking at was had this copy pasted over, and over, and over again:
someValue, err := someFunction();
if err != nil {
panic(err)
}
I'm obviously not an expert, but it might be of value that it's only taken me my first glance to end up on this thread.
That's because you're looking at code snippets for learning. Real code has to handle errors, not just panic and crash.
True, but errors can (and often should) be grouped. That's why try/catch blocks exist in other languages. For example, the following would smell much less like dinosaurs to me:
try {
foo, throw err := someFunction();
bar, throw err := foo.get();
baz, throw err := bar.make();
qux, throw err := baz.transform();
} catch(err) {
// "Unable to foo bar baz qux."
tryHarder();
}
Once again, total layman. But code is just symbols, and if they repeat enough, you can make a symbol for that too. This appears to be a very frequently repeating symbol.
You might want to take a look at Rob Pike's Errors Are Values post to see how you can use a helper to merge errors and deal with them all at once. In practice catching all exceptions with a single clause is considered bad style in most languages that have them, because you end up hiding information about what actually happened. (And if you extend the example to break out the individual caught exceptions and not throw that information away, the code ends up as long as the Go equivalent.)
Thanks for the link. The errWriter
is a totally passable solution.
True, but errors can (and often should) be grouped. That's why try/catch blocks exist in other languages. For example, the following would smell much less like dinosaurs to me:
try { foo, throw err := someFunction(); bar, throw err := foo.get(); baz, throw err := bar.make(); qux, throw err := baz.transform(); } catch(err) { // "Unable to foo bar baz qux." tryHarder(); }
Once again, total layman. But code is just symbols, and if they repeat enough, you can make a symbol for that too. This appears to be a very frequently repeating symbol.
Let's say each function returns overlapping error type and you must handle all function result gracefully, how do you write tryHarder()?
try {
foo, throw err := someFunction(); // err could be TypeA and TypeB
bar, throw err := foo.get(); // err could be TypeB and TypeC
baz, throw err := bar.make(); // err could be TypeA and TypeC
qux, throw err := baz.transform(); // err could be TypeB and TypeD
} catch(err) {
tryHarder(); // tell me how to handle each error?
}
It will only take someone else 1 minute to understand the below code:
foo, err := someFunction(); // err could be TypeA and TypeB
if err != nil {
// handle err
}
bar, err := foo.get(); // err could be TypeB and TypeC
if err != nil {
// handle err
}
baz, err := bar.make(); // err could be TypeA and TypeC
if err != nil {
// handle err
}
qux, err := baz.transform(); // err could be TypeB and TypeD
if err != nil {
// handle err
}
Let's say each function returns overlapping error type and you must handle all function result gracefully
In that example, you're totally correct.
Most helpful comment
there should be only one way of doing a thing