-
Notifications
You must be signed in to change notification settings - Fork 17.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: A built-in go error check function, "catch" #32811
Comments
For the record, I don't mind However, if we're going to change the language, this is a much better change than the proposed IMO it removes the major faults of try, which are the high likelihood of missing a call to |
One variation would be:
decorate := func(err error) error { return fmt.Errorf("foo failed: %v", err) }
...
f, err := os.Open(config)
catch(err, decorate) // decorate called in error case or using helper functions that could live somewhere in the standard library: catch(err, fmt.Annotatef("foo failed for %v", arg1)) That would be more parallel to the current proposal for edit: part of the rationale for a handler function is it means a builtin does not directly rely on |
My main reason for disagreeing with that is that it couples the |
That's a good point, and actually why I made 2a and 2b. I think we could remove 2b pretty easily. It could easily be replaced by a helper function in fmt (or user code): // Wrap returns nil if err is nil, otherwise it returns a wrapped
// version of err with the given fmt string as per fmt.Errorf.
func Wrap(err error, msg string, args ...interface{}) then you could do catch(fmt.Wrap(err, "error opening config for user %s", user)) It's not quite as nice, though. And for the record, I'd be ok with making this part of fmt be part of the language if it encourages people annotate their errors. |
@natefinch What about if i have more than two returns?
|
It works like so like if you had func getConfig(user, config string) (*Config, result, error) catch(err) would |
You can simplify this a bit. Since it expects an error value, you can give it any error value. So then it can be
Except for the name, this would be a perfectly legitimate use at the where error conditions are first detected and even better than having to say
On IBM systems on can use a macro ABEND to abnormally terminate a task. It was a bit like |
So, it can't be a keyword, because that's not backwards compatible. So it would need to be I do agree that the name might need some tweaking. Naming is hard, and I welcome suggestions for another name that might be more appropriate. The problem is really that it is most accurately called How about f, err := os.Open(config)
yield(err)
defer f.Close() and then it could be yield(OutOfRangeError) which looks better to me |
(Ignoring its use as giving up the processor to another coroutine or thread) yield is better than catch! returnIf can also work provided error values have err or error in their name.
|
Both But I have to disagree with this if for no other reason than its rigidity; if catch(err, "can't open config for user %s", user) is really just if err != nil {
return fmt.Errorf("can't open config for user %s: %v", user, err)
} ... it's really not that valuable. Most of my error handling is done with But to think this through further... what happens in this case: func DoYourThing(a string) (x string, err error) {
x = fmt.Sprintf("My thing is '%a'", a)
// always returns an err
err = DoSomethingBad()
catch(err)
// process some other stuff.
return
}
func DoAThing() {
x, _ := DoYourThing("cooking")
fmt.Printf("%s!!\n", x)
} |
@provPaulBrousseau, IMHO catch or its equivalent shouldn’t be used if you are returning an error as well as a legit value. [This is another reason I bemoan the lack of a sum type in Go. With sum type you’d use it in most cases where an error or a legit value is returned but not both. For examples such as yours, a tuple or a product type would be used. In such a language But if you do use |
@provPaulBrousseau note that my intent is that this use the new fmt.Errorf which actually does wrapping like github.com/pkg/errors (which is what we use at work, as well). So that fmt.Errorf looks like the oldschool "just reuse the string", but it's actually wrapping under the hood. I'll update the original post to make that clear. Certainly, if you want something more complicated, like turning one error into some other type of error, you'd instead use the methods you used before, with an if statement. This is just trying to remove a little boilerplate and remove some friction from adding context to errors. |
oh.... and yes, just like |
Option catch takes an If named return parameters are used, then the function returns with those values and the supplied error when the error is not nil. It's a compile time error to use catch in a function that does not have an error value as it's last return. Examples: func Foo() (int, int, error) {
err := ....
catch err // nil processing continues, !nil return 0,0, err
user := "sally"
err := ....
catch fmt.Errorf("can't open config for user %s: %w", user, err) // note new %w flag, so it's an `error` and an `errors.Wrapper`, nil processing continue, !nil return 0,0, <formatted error> I like this for all the reasons you pointed out. And there is less "magic" wrt formatting, and if after a release or two it's felt that formatting should be built in it can still be added later by allowing the catch statement to take multiple arguments.
|
PS: I realize this would require a tweak to func Foo() (int, int, error) {
user := "sally"
err := ...
catch errors.Wrap("can't open config for user: %s: %w", user, err) |
This seems like a non-starter. Can it just be a |
WRT Backwards compatibility ( |
At first glance, this is generally what |
@apghero why is it a nonstarter? That's basically the whole difference between this and the try proposal. I do not want a function on the right hand side of a complicated statement to be able to exit. I want it to have to be the only thing on the line so it is hard to miss. |
@daved |
This is many ways worse than try() function proposal in my opinion because it doesn't really solve the underlying goal of reducing boilerplate code. A function with multiple error check looks like this:
Vs. originally proposed try() function:
Later looks more concise and elegant in my opinion. |
@ubikenobi It's always trade-offs. In my opinion, the reduction provided by |
Me either. But that can be accomplished without syntax. The syntax part is the thing that breaks backwards compatibility, so why not just do what |
This still would take up a keyword: catch, but only barely improve on the try variant. What for users that use custom errors that use wrapping functions? if err != nil {
log.Println("Hey operating, big error, here's the stack trace: ...")
return myerrorpackage.WrappedErrorConstructor(http.StatusInternalServerError, "Some readable message")
} If we plan on taking up a whole keyword, can we at least attach some function to it? Suggestion is part of the whole mix of error handling proposals: scoped check/handle proposal |
The point is to reduce the footgun rating of a solution, not to offer an "improved" novelty.
This was covered in comments.
Now I'm not sure if this is a troll post. Most keywords serve exactly one function. |
This proposal states several times that |
I guess you should close this proposal as the one to keep it simple and keep it as it is got way more positive attention... 💩 |
Thank you sir, I do think Go should allow implicit casting from nil interface to bool. |
Everyone seems to have a different opinion on error handling ;) I'd be fairly happy with that solution too - allow if err {} on one line, and simple error handling would be a lot more succinct. It would perhaps have a more wide-ranging impact though (allowing implicit conversion of non-nil to bool in all code). Not sure all the heat and noise around this issue is warranted, but I would prefer a solution that shortens error handling and doesn't insert itself into the happy path instead (as try does). |
I'm just gonna throw this out there at current stage. I will think about it some more, but I thought I post here to see what you think. Maybe I should open a new issue for this? So, what about doing some kind of generic C macro kind of thing instead to open up for more flexibility? Like this: define returnIf(err error, desc string, args ...interface{}) {
if (err != nil) {
return fmt.Errorf("%s: %s: %+v", desc, err, args)
}
}
func CopyFile(src, dst string) error {
r, err := os.Open(src)
:returnIf(err, "Error opening src", src)
defer r.Close()
w, err := os.Create(dst)
:returnIf(err, "Error Creating dst", dst)
defer w.Close()
...
} Essentially returnIf will be replaced/inlined by that defined above. The flexibility there is that it's up to you what it does. Debugging this might be abit odd, unless the editor replaces it in the editor in some nice way. This also make it less magical, as you can clearly read the define. And also, this enables you to have one line that could potentially return on error. And able to have different error messages depending on where it happened. Edit: Also added colon in front of the macro to suggest that maybe that can be done to clarify it's a macro and not a function call. |
It's a much larger change than |
@Chillance You're essentially proposing to introduce an hygienic macro system in Go. This opens another can of worms. An issue was created proposing this: #32620. Here is what it looks like in Rust: https://doc.rust-lang.org/1.7.0/book/macros.html. |
@ngrilly Yeah, I'm sure there other things to consider for the macro way. I just wanted to throw it out there before things are added too quickly. And thanks for the link issue. Great to see others brought it up too. Naturally, if macros would be overused it could get complicated, but then you have to blame yourself writing the code like that I suppose... :) |
@Chillance I'm personally opposed to adding hygienic macros to Go. People are complaining about try making the code harder to read (I disagree and think it makes it easier to read, but that's another discussion), but macros would be strictly worse from this point of view. Another major issue with macros is that it makes refactoring, and especially automated refactoring, so much harder, because you usually have to expand the macro to refactor. I repeat myself, but this is a whole can of worms. try is 1 year old toy compared to this. |
@ngrilly My initial thought with Go macros is that they would be simple, shorter tidbits. And you would use those in the function to make it easier to read. That is, not overuse macros. And, use for simpler things that you don't want to repeat, such as error handling. It is not suppose to be very large, because then just use a function I suppose. Also, having a macro, doesn't that make refactoring easier as you have one place to change things instead of maybe many in the function? |
@Chillance The problem with macros is that tools (especially refactoring tools) have to expand the code to understand the semantics of the code, but must produce code with the macros unexpanded. It's hard. Look at this for example in C++: http://scottmeyers.blogspot.com/2015/11/the-brick-wall-of-c-source-code.html. I guess it's a little bit less hard in Rust than in C++ because they have hygienic macros. If the complexity issue can be overcomed, I could change my mind. Must be noted it changes nothing to the discussion about |
Why not use existing keywords like: func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
return if err != nil
defer f.Close()
return ioutil.ReadAll(f)
}
f, err := os.Open(filename)
return if errors.Is(err, os.ErrNotExist)
return x, y, err if err != nil A more complicated example doesn't seem to have advantages in terms of readability compared to the traditional if clause, though: f, err := os.Open(filename)
return nil, errors.Wrap(err, "my extra info for %q", filename) if errors.Is(err, os.ErrNotExist) |
This seems to be nothing more than an equivalent to a macro such as the following:
Why not just make macros a feature and let users implement their own shorthands? Could even shorthand the decoration
|
Because we have no time to learn every other library's own flavor of Go just to be able to comprehend what is it doing. We've been there, done that @ C, we've been suffocated by it in C++. RiP Rust was such a wünderkid until got the feature-fever and passed away.
...and possibly will send user keys to the moon in yet other decoration you'll skim over. |
@natefinch func check(Condition bool) {} // built-in signature
check(err != nil)
{
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, // dress before leaving
fmt.Errorf("Can not get user's Calendar because of: %v", err)
}
// happy path check(x < 4) // implicit: last statement is of the sad path
{
x, y = transformA(x, z)
y, z = transformB(x, y)
x, y = transformC(y, z)
break // if x was < 4 after any of above
} Last one already compiles at playground. @Merovius [about print/println]
You're right. I was about reusing |
I agree with @Merovius that we can't put Of course, then the new built-in doesn't help with error annotation, which is one of the objections that people raise about The name |
As a fallback position regarding the So under that alternative, a wrapping example could be:
A similar-in-spirit solution would be to have an error handler as an optional second argument to
Some of the concerns that have been raised in the discussion about the
As far I understood the discussion, I think this proposal targets all three of those. One final question / comment: if a builtin such as |
FWIW, this code would also work with The former seems fairly minor. The latter seems more significant and will essentially require the builtin to be used in a statement context. Rolling out |
As I understand it, the proposal as it stands is:
The advantage is that the block A disadvantage is that the The proposed Does this seem like a correct summary of the current state? Thanks. |
That is my understanding. However, this was a counterproposal to try, which has been discarded. I don't think this proposal actually solves that much, I mostly did it as a compromise between people that wanted something like try, and those that thought try would hurt readability too much. |
Do you want to withdraw the proposal? The votes on it aren't all that great. |
I'm happy to withdraw it given the votes. |
Thanks. |
This is a counter-proposal to #32437
Proposal: A built-in Go error check function, catch
catch
would function much like the proposedtry
with a few specific differences:1.)
catch
would not return any values, meaning it must be on a line by itself, likepanic()
2.)
catch
takes 1..N arguments2a.) the first argument must be of type
error
2b.) The remainder of the arguments are optional and wrap the error using the given format string and args, as if sent through the new fmt.Errorf which does error wrapping similar to github.com/pkg/errors.
e.g.
In this code, catch is the equivalent of
If err is non-nil, it will return zero values for all other return values, just like
try
. The difference being that sincecatch
doesn't return values, you can't "hide" it on the right hand side. You also can't nestcatch
inside another function, and the only function you can nest inside ofcatch
is one that just returns an error. This is to ensure readability of the code.This makes catch just as easy to see in the flow of the code as if err != nil is now. It means you can't magically exit from a function in the middle of a line if something fails. It removes nesting of functions which is otherwise usually rare and discouraged in go code, for readability reasons.
This almost makes catch like a keyword, except it's backwards compatible with existing code in case someone already has the name catch defined in their code (though I think others have done homework saying try and catch are both rarely used names in Go code).
Optionally, you can add more data to an error in the same line as catch:
In this configuration, catch is equivalent to
And would utilize the new implicit error wrapping.
This proposal accomplishes 3 things:
1.) It reduces if err != nil boilerplate
2.) It maintains current legibility of exit points of a function having to be indicated by the first word on the line
3.) It makes it easier to annotate errors than either if err != nil or the
try
proposal.The text was updated successfully, but these errors were encountered: