go: proposal: `try...or` for right-shifting error handling
Here’s yet another error handling proposal. I’m warming up to the idea of keeping error handling with the erroring code, but I’m still hoping we can find a way to better emphasize the primary code path.
I noticed that a particular try syntax yielded 40 positive emoticons and no negative emoticons, but a far more popular assessment of the try syntax pointed out that it was hiding error handling behavior.
I thought to employ the try syntax without hiding error handling. Under this proposal, the following code:
w, err := os.Create(dst)
if err != nil {
return err
}
defer w.Close()
if _, err := io.Copy(w, r); err != nil {
// additional cleanup goes here
return err
}
… would take the following form:
w := try os.Create(dst) or return _err
defer w.Close()
try io.Copy(w, r) or {
// additional cleanup goes here
return _err
}
If the or keyword isn’t appealing, maybe someone could suggest an alternative keyword or alternative syntax. Same goes for the proposed auto-assigned _err identifier, which would hold the last return value.
This approach moves all error-handling code to the right, so the primary code path is more visible — less cluttered — along the left. The try keyword puts some clutter on the left, but because its syntactically simple and always the same, I’m thinking we would easily gloss over it, especially with 4-space tabs when there is no other return value.
UPDATE 1: Any statement or bracketed code block can follow or, and _err would be scoped to that statement or code block.
UPDATE 2: Scroll down a few comments to see simpler variations of this syntax that drop the word try.
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 15
- Comments: 39 (9 by maintainers)
For what it’s worth, I think the current way of handling errors is one of the nicest out of any language I’ve touched, even factoring in its repetitiveness and verbosity. To me, it’s not a problem worth solving.
But it also removes the ability to manipulate the value before returning. Since
errors.Unwrap()was added, I’ve tried to make it a habit to almost always wrap errors with information about where it happened and what it was doing. I very, very rarely just directly return an error anymore.I like the idea of reusing
else. It might give me a reason to actually use that keyword now.?would make it impossible to use that operator for nil-safe access operations, though.I don’t like that, either. Maybe an explicit declaration?
If you wanted to reuse an existing variable, you could just do it explicitly still:
I find the implicitness of
error_errlittle transparent.feels clearer for me (as long as try can only address a single statement). It remains to be defined what scope
errwould have.If the additional declaration or
errneed be saved and the often-used case of just returning the error is concerned you might shorten that toThat would need an additional rule of returning zero values for all other return values.
This proposal itself may not be it, but I think the use of the else keyword seems very interesting.
statement1 else statement2 else statement3 ...This could have the semantics that statement 2 is executed only if statement 1 evaluates to an error, or to an assignment including exactly one error, and so on.
@jtlapp Thanks. We try to avoid that by encouraging people who do not currently use Go, but are interested in it, to take our annual survey, where we ask questions like “why do you not use Go more?” See, for example, https://go.dev/blog/survey2020-results. Of course it’s impossible to avoid all biasing. All we can do is try our best. And at the same time we recognize that the language is not for everyone.
elsemight make this variation more readable and spare us any additional keywords:Compare with the original code:
Hey @ianlancetaylor. Maybe your team has already considered this, but when you decide what issues to fix according to the polling on this repo, you are only appealing to your current programmer base and not making an effort to expand that base. It is possible to keep the current crowd happy while drawing in others with features they’re looking for.
For myself, I keep revisiting Go when I need to do heavy lifting, but each time I’m hesitant to commit the next project because I value writing code that can be understood at a glance, without having to mentally filter out clutter. IMO, hard to read code is hard to maintain in the long run and more prone to problems, although Go certainly offsets this in other regards.
(And Go devs, I know you disagree with this assessment, but I also know you know Go turns off a good number of programmers. I’m speaking for those who are intrigued but too concerned to commit.)
Eek! That’s confusing. I think you’ve killed the proposal, as written.
What about
try f1() or return try f2() or return _err? Which error gets returned?@bronger
Zig’s error handling has been mentioned in a number of proposals before. It was discussed a fair bit in #55026.
I don’t see any evidence that returning a received error verbatim is considered bad programming. Besides, I don’t see why. There are use cases for wrapping an error, but there are other cases where the original error already says enough. Also note that each language has to be able to scale down, i.e., make simple things simple. Most Go projects are not big, yet their needs deserve being covered by syntax.
@bronger
The problem is when a proposed new notation incentivizes bad coding practices by making them easier. In general, blindly returning error values without wrapping them or handling them is bad code, and in a good codebase it will be done rarely. If you give programmers a way to avoid doing anything with errors by adding
else returnto every call to pass them all the way up tomain(), a lot of programmers will do just that. Just look at how many Java programmers took tocatch (Exception e) {}.That’s why in previous discussions, people have repeatedly said that they want to see a proposal that makes good error handling easier, and not just something that makes
if err != nil { return err }easier.@DeedleFake Your objection clearly shows, however, that my point (10.) should be deleted. If one wants to return something different than the naked
errreturn value of the called function, it will highly probably base onerr. So there is no benefit in having something after “else return”.Your comment with
boolvalues is also helpful: The constraint that it must be comparable tonilcan be lifted by saying we just compare to non-zero and that the types of the two last return values must match. This even strengthen the paradigm “errors are values” by not special-casing any types.I will update my main comment above accordingly.
I do not agree. FWIW, I look at the variables left to the equal sign and know what is returned.
Moreover, I think that any proposal that covers the simplest cases is enough as we have always a decent default notation (i.e. the current way) for the more complex cases. Still, in Go the happy path is perpetually interrupted by the unhappy path, plus things like
if err := myfunc(); err != nilwhich move the happy path (the function call) away from the start of the line.Therefore, I’d like to point the focus on a simplified version of what is proposed above:
What this does:
The last return value must be comparable to nil.If the last value is notnilits zero value, the current function returns, and the last value is returned as the last value of the current function. The other return values of the current function are their current values (if named) or zero values (if unnamed).But if the last value is nil, all return values except the last one are assigned to the LHS.
I see the following nice properties:
else returnbeing?, and might be attractive to people coming from such languages.Has the optional extension that one can give the returned things explicitly after thereturn. Personally, I’m indifferent here.I’ve thought of a serious problem with the variants of this proposal. It’s confusing to have both of these lines of code valid:
We really need a marker up front to tell the dev that we’ve skipped a return value, as otherwise the dev will have to look ahead to the end of the function call to decide what is being returned.
I’ll close this issue, as both the original proposal and its variants are problematic.
You told me that I don’t value simplicity, but we can talk about simplicity of the Go specification, and we can talk about simplicity of the code. I’m uncomfortable with Go exactly because it makes the primary code path too complex – complexity that most Go devs are apparently comfortable with. There is too much boilerplate code obscuring the normally intended operation. Even the original RFP admits this undesirable complexity.
That’s the slippery slope argument, which I think is more dismissive than constructive.
I’m actually trying to remove syntactic features from the primary code path, in order to make the path more visible. Because of the way error handling was implemented, some new feature will be needed.
For a while, but eventually you end up with C++.
There are very few programming languages that try as hard to keep simplicity as a feature of the language. I’m inclined to say that if you don’t value simplicity over features, Go is probably the wrong language for you.
Perhaps the way to save it is to disallow
orchaining, but that’s an odd constraint for an otherwise-generative language.@ianlancetaylor, Yes, I saw both of those before posting this proposal, but thanks for including the links. It was the popularity of this comment that made me wonder if a slight twist on prior solutions would make people happy. So far, that doesn’t seem to be the case.
My suspicion is that this community isn’t concerned with the issues identified in the original RFP. Maybe a poll would ascertain what error handling issues are worth focusing on, if any. If most select, “error handling is fine as it is,” then we needn’t put another ounce of effort into it.
It doesn’t. It offers an additional way of expressing things when that manipulation is not needed. I’ve presented both together, not either or.