go: proposal: allow implicit conversion of error to bool in if statements, fmt on one line
A couple weeks back during a proposal review, reviewing the fiftieth or so error handling proposal, I made a somewhat flippant remark that if we made if err != nil {\n return nil, err \n}\n shorter, most of the error handling proposals would go away. (At least the more incomplete ones)
I then mentioned two things we could do pretty easily:
- allow, in
ifstatements only, just:
if err {
… as syntactic sugar instead of:
if err != nil {
That is, if the static type of a variable is exactly and only error, allow conversion to a boolean.
“But that’s not orthogonal, why would you make error special?” Yes. But errors are already special. They’re the only interface predefined in the language. And they’re incredibly common.
Even if somebody is momentarily confused why it works, it’s at least not ambiguous.
IIRC, we discussed a bit whether to allow it in all boolean contexts, just in boolean expressions inside an if condition (so if !err { would work), or only when it’s the entire expression in an if (only if err {).
- modify gofmt to format those on a single line (previously proposed and rejected in #27135):
if err { return err }
Later, if we do #21182, that extends to:
if err { return ..., err }
But if you have any non-return statement in the if body, then it formats as multiple lines.
Taken together, this makes error handling much lighter looking (the common motivation for new error handling proposals), while keeping flow control unchanged and explicit (a common argument against most error handling proposals).
I made these comments mostly as a joke at first, but the more we talked about, the more we didn’t outright hate the idea. I reluctantly agreed to file an issue, so here it is.
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 170
- Comments: 60 (23 by maintainers)
I like the idea of being able to test for errors this way, but I really feel like if you were going to make this change, that you would just make the bigger change for truthy testing any value. I get that error is a special case interface but I don’t think users are likely to think of that when they find they can only truthy test an error interface and not any type. It will feel like a quirk where the language is unbalanced. But I do remember the reasoning for not having it for all types being something like it leads to confusion or bugs when you mistake the type you are testing? I don’t know about that. You still have static typing and no-implicit-conversion to protect you later when you use the value in other ways. What is the downside of this example?
@peterbourgon there is and it’s called bad language design. Language should be consistent and intuitive. If someone sees
if errthen the only logical conclusion is that error types can be implicitly converted to booleans (and the proposal clearly says “allow conversion to a boolean”). That means all the following cases should also workThe last one is unfortunate consequence of the proposal but it must work, otherwise the whole thing becomes a mess of special case syntactic sugar that doesn’t mean anything (in other words, it no longer means implicit conversion to boolean. It now means “we like how this looks, let’s allow it”).
@creker
There’s nothing wrong with this.
Only after
ifand only if it’s the whole expression? If so, sounds good.Would this also work with annotated (wrapped) errors, e.g.
fmt.Errorf("annotation: %w", err)? If so, sounds good; if not, strong -1.I had actually explored this same idea myself, around the time when the other error proposal was being circulated. My conclusions/worries in respect to this specific proposal:
The special form of
if err {might make other error handling constructs that do not fit the exactif err != nil {form seem out of place. This may lead programmers to refactor such code such that they can writeif err {instead, which seems counterproductive.Having the error handling on one line isn’t desirable IMO. Within the context of a function a return statement does something profound: it terminates execution. For that reason I think a return statement deserves its own line—with indentation should the return only happen conditionally—as it does today.
I think the consistency of having one way to check errors—arising naturally from the orthogonal concepts of expressions, if statements, blocks, and return statements—is a benefit higher than saving a few keystrokes and line breaks.
@creker, writing:
… separates the word “if” from the word “err” (error). Since it no longer reads “if error”, I think it’s totally reasonable to require that be written the normal
err != nilway, with no fmt changes.@bradfitz it’s fine if gofmt doesn’t format it as a one liner but it doesn’t make sense disallowing
That just means language spec would have some strange rule about completely identical code (apart from err scope which is irrelevant here) being treated differently by the compiler. I would expect Go to be consistent in its syntax.
@creker,
We definitely would not want
var _ bool = errto work. Only forif.Yes, this proposal is just syntactic sugar. But that’s kinda the point: it stemmed from a discussion of what the minimal sugar we could add to appease the largest number of people without much more intrusive (and probably controversial) language changes.
Limiting the if to only contain a return/break/continue makes it not apply for testing, like
which that style I think would help remove the desire and proliferation of incompatible assertion style testing libraries.
For those situations, one could even consider going further and adding brace alignment to aid visual scanning:
But, there’s no existing precedent to do that alignment with function or struct literals like there is for allowing the whole thing to exist on a single line.
During the
tryproposal, I also brought the single line if statement idea up (https://github.com/golang/go/issues/32437#issuecomment-499161412) and many of the points I describe there also apply here:gofmtform.In my estimation, the amount of win you can get by allowing the discerning author to place if statements on a single line easily pays for the complexity and implementation cost. It has no negative interactions on existing or future code, greatly reducing the risk. And since there have already been changes to
gofmtthat changed existing code’s formatting, the path to reverting it in the future is open and minimally expensive.As far as worries about abuse, there are already a multitude of places where
gofmtallows the author to choose between one line or multiple: functions (literals and declarations), struct/map/slice literals, type definitions, allowing semicolons on a single line (https://play.golang.org/p/Yr0bfdQPTrp), vertical spacing between imports, and I’m sure more. Yet we rarely see any of those absued.I think it’s a very nice gradual step forward to reducing the page weight of error handling, even if it doesn’t reduce the amount of ink on the page.
@creker
Yes, it does.
@peterbourgon
That’s exactly the problem, it’s not. Like I already said, there’s no objective reasoning here (you still didn’t give any argument as to why it should work like that) and it goes against common sense. When you learn a new language feature you automatically try to apply it in a different context. This new sugar looks like implicit boolean conversion and works like that. The logical conclusion would be to try to use that conversion everywhere where you can already write “err != nil” or some other similar expression. That means, someone would definitely try to write all of these
These examples are intuitive, logical, easy to explain and teach in one simple sentence - errors are implicitly convertible to boolean. That’s it. What we have here would confuse people, would not give any explanation apart from “that’s how it is, deal with it”, require much more specification changes and just turn Go into something it is not. It’s never been a language where you’re not sure if something you write is a correct syntax. But I agree that we don’t want to allow all of those examples, so to quote you again
I agree but it would be even more so to implement this just to introduce some cute syntactic sugar.
I am new to the Golang party and aware that there is a ton of past proposals that I have not read, but might as well wade into the arena:
This proposal is nice in that it 1) accepts the reality that errors are special and unavoidable and 2) makes a minimal change for ergonomics that while are admittedly special-cased, are of such commonality as to fade into the background once adopted. I think of the semantics for how
makeoperates on specific types. Why are slices and channels sufficiently special so as to demand language affordances that errors (which are far more common) do not?I think when combined with an affordance that enables the low ceremony return of underlying errors, we can address 80% of the pain without introducing new keywords, flow control, etc.
To highlight what I mean, consider the introduction of a new error specific sentinel value (and maybe this should be a parallel proposal but this is where I am right now) of
^that wires in the semantics of return error if not nil – very similar to how_throws away a value you don’t care about.Consider something like:
IMO boilerplate check and return of errors and coercion to boolean is the essence of the error handling ergonomic issues
I think both of these look even worse than
if err != nil.Implicit bool-ish-ness is one of programming’s greatest mistakes, and that Go doesn’t have it is one of its greatest strengths. This exception to that rule is specific, easy to explain/teach, and easy to understand. It would be the acme of foolishness to expand this exception into general bool-ish-ness for all types in the name of consistency.
That said, I don’t really have any issue with error handling as-is, and I don’t think the language needs this addition.
I think it’s good to be consistent and logical up to a point, but syntactic sugar is frequently special-case exceptions. We already have unusual rules on the behavior of if statements; the “if a := expr; a != 23” construct isn’t a way you can spell an expression in any context other than an if statement, and it’s not quite equivalent to having the first expression simply before the if statement, because it has different scoping.
I would prefer “an if statement can take an expression which is either any expression of type bool, or a single identifier referring to an object of type error” to “automatic conversion of some things to bool”. I don’t think the proposed change here would have much impact on my daily life, but I’d probably use it. A change allowing error in general to be used as a conditional expression, or pointers or interfaces in general, would be a major change, and I’d guess that I’d end up being hurt by it more than I’d be helped by it. (Although I’m not sure; I got very used to “if (p)” in C, and sort of miss it sometimes.)
I would be pretty strongly opposed to a general “implicit conversion to boolean”. I would not object to conditional operations, like for or if loops, being able to take interfaces, or pointers, or possibly just error in particular.
As it gets more general, it gets more confusing. Consider:
I would be thrown off by this even given the assumption that “err” could be used as a control expression for an if statement. It wouldn’t confuse me in C, but in C, we’re already used to pointers being usable as truth values.
That said… What about cases where we’d return early on success?
At the risk that it will make this proposal seem dangerous, consider:
This, coupled with the above, makes me think that this absolutely should not imply a general conversion of
errortobool, but rather, should be a special case of theifstatement.@peterbourgon, returning wrapped errors seems reasonable, yes.
C++ defines explicit conversions to bool as being special in the language, as they can occur implicitly in certain contexts. For example, if x is explicitly convertible to bool, then
if xwill call that conversion. However, trying to pass it into a function that accepts a bool won’t work, because its not implicit.I believe a similar approach would work in Go. Allow the error type to be explicitly converted to bool using the conversion operator as in
bool(err), but also allow the conversion to occur implicitly in certain contexts, specifically inside an if statement condition. It would have to allow the!operator to convert it to a bool for consistency.This creates a small set of rules that to me seem very Go-like, as they are simple and can’t be leveraged and overloaded by the user; it’s just a special case.
On the other hand, if we were to simply allow the specific syntax
if err, this will inevitably lead to confusion, as users will wonder why other constructs likeif !err,if err := f(); errandif err1 && err2don’t seem to work. This kind of unexpected behavior is what I think a language feature should avoid.It is similar to PHP’s
(int)and(float)constructs, designed to look like cast operators from C like languages to be familiar to users. However, they are misleading because there are nointorfloattypes in PHP - the casts are just special syntactic features. It is very much like this, because a user will assume some conversion to bool is happening, where none is at all.Instead of a language change, I’d prefer a (non-default) switch in go fmt that preserves certain single-line constructs, e.g.
I’d be disappointed to see another error handling discussion engulf a lot of Go team time.
BTW, how’s generics coming along? 😃
@bradfitz in that case the proposal description is misleading. There’s no “allow conversion to a boolean” going on any longer. The proposal now is about liking some piece of syntax that doesn’t exist in the language. Meaning, before compilation
if err {is implicitly unfolded intoif err != nil {by the compiler. In that case I no longer like such a proposal. It would be fine to explore this as a proper language construct but, as you describe it now, for me this is not worth polluting the language.Overall, I like how refreshingly simple this proposal is, compared to most other error handling proposals. It deals with the verbosity without adding too much magic.
I think this rule could be simplified to: this kind of if statement is allowed to fit in a single line if its body is exactly one statement, and its length is short enough.
This rule is already in place in
gofmt, for single-line functions. The heuristic is just not explicitly defined, because it might change in the future. See it in practice - click “format” on https://play.golang.org/p/rEyBUh5rj7VI think another point in favor of the
handleproposal was to consistently wrap errors in a function:Note how we repeat the error wrapping/decoration. This is good if we want the call site to decorate an error with the function that was called, but I thought the point of the go2 draft was also to encourage functions to decorate errors with their own information when returning.
@peterbourgon would be very strange that this
is allowed but this
is not.
@justinfx the problem is that empty slice and nil slice are both useful and practically identical in most of the cases (
len,cap,appendall treat them equally). But your example would probably handle them differently and cause mistakes and confusion.I’m not in favor of implicit error-to-bool, but
if bool(err)isn’t helpful. And the lion’s share of this proposal can already be achieved by an alternative to go fmt.I’d be disappointed to see another error handling discussion engulf a lot of Go team time. We were told the topic had been shelved after the community voted overwhelmingly against making any change. That was a well-considered decision.
Would
if f() {be legal withfunc f() error {?Imho introducing this feature specific for errors looks like workaround. If this proposal will be considered, it makes sense to add this behavior to other
nil-able items (and mb numbers).But still not sure that this is a good way to solve the good-old
if err != nilissue.@swdee, there are plenty of proposals already open for new keywords like that. This is specifically what this issue is trying to avoid.
The main benefit of guard in Swift is not saving a few lines but optional binding. Other than that I always considered it as a pretty strange construct. It’s an if statement and Go doesn’t need two types of if statements. I’m sure the proposal would be quickly downvoted and declined.
As for extending an if statement, there were multiple such proposals and all of them were declined either because people don’t want another special magic symbol to do all kinds of things (like ^ in the above example) or it doesn’t address all the requirements (like allowing for errors to be augmented with some contextual information). The example above have the same problem many other such proposals had - it makes it difficult to see the error path. You have to scan whole line to get to it. It also makes lines much longer and will require you to split them in multiple lines possibly making code even harder to read. The benefit of current error handling is it’s completely linear - you can scan the function from top to bottom only checking a few starting symbols on each line and get a glimpse on every return path.
@blakewatters I really like your idea for its simplicity and resemblance to
_. You could go fancy and even add error wrapping:which is a bit hard to read with a later-called function on the left hand side. We could also do something similar to extended
ifstatement syntax:or, to make the
returncase explicit:The last two variants could be combined with non-zero values to the non-error returns, too:
Different from this proposal this would allow to simplify a lot of error return statements to concise one-liners. Would it make sense to open yet another error proposal for this?
One thing to consider about allowing single line if statements is that
is not syntatically correct due to automatic semicolon insertion, so some sort of change or decision would have to be made there.
@creker,
I guess I was more explicit if you read the title of the issue itself. The next words are “in if statements”.
That I don’t know what it means. You might mean “if s is non-nil”, you might mean “if s has a length other than zero”. Heck, you might mean “if s contains at least one string which isn’t an empty string”.
I think it’s pretty reasonable to only accept the exact form
if errand not the longer form with the prior assignment. It’s syntactic sugar, it can be narrowly-focused, and I’d definitely prefer the != nil to still be present in the longer lines so I have an easier time reading them.if a, b, err := threeReturns(...); errmakes it a bit too easy for me to miss the nature of the condition.
if a, b, err := threeReturns(...); err != nilis clearer.
The argument here is just that, for the very specific case of
if err != nil, allowing the != nil to be omitted doesn’t obscure things, because there’s no other expressions nearby that can visually subsume theerr.I don’t think this breaks the syntax much. The proposal here isn’t to allow single
errorvalues to be truthy expressions; it’s to allow the special caseif errorValueas an alternative to requiring if statements to take truthy expressions.As a C programmer, I often miss
if ptrconstructs, but I recognize the confusion they can introduce.Would you want this to work also for
if a, err := foo(); err {?I have actually been thinking about this same thing, because I frequently end up with, roughly:
and what I really want for a lot of these is basically “return these values if [this condition involving these values]”. But in 99% of cases, the condition is “the last one, which is an error, is non-nil”.
the ugly answer:
erreturn a, errWill wait some more to not raise this twice. A connection of @ydnar‘s suggestion of try/guard statement that is able to elevate variables out of block scope (which was my biggest pain) allows for using syntax similar to the if statement plus introduce a short form that entirely avoids the multi-line if err checking. I wouldn‘t want to raise it and waste everybody‘s time though if this has been seen before- I couldn‘t find it in the error handling-tagged proposals.
The
guardstatement in Swift assigns variables in the outer scope, as syntactic sugar for an if statement:If Go adopted it:
@blakewatters i like this idea for simple apps. But most of the time i prefer to wrap the error to add context. I think this idea was already discussed in the
tryproposal, but maybe you should still make a new proposal ?Neat idea, but IMO it reduces clarity. Something like
?to do the conversion would at least let the reader know what’s happening.if err? { return nil }(Probably already several proposals involving a question mark floating around…)i believe your solution is more idiomatic as per language , and mine is more beginner friendly
@jimmyfrasche, no.
@bradfitz what if you allowed Ruby-ish
ifstatements following areturnstatement, coupled with implicit zero values for non-errortypes?return err if err != nilI’m fine with 2. I’d prefer if it’s further limited only to if’s that contain a return/break/continue statement. That’s not especially important to me.
1 makes me uneasy.
If it’s just limited to
error, does it mean I can’t use it with a value ofnet.Error? I can see why not, but overall the definition seems a little too special. I don’t think it’s going too far to just extend it to any interface value even if the target is specificallyerror.Other boolean contexts seems like it could get convoluted. I could see
if !err {as a single special case but I think disallowing it wouldn’t provide any problems. Writingif err == nilhappens but it’s fairly rare, ime.I don’t have any problem with
if x := f(); x {}. It looks exactly as clear to me. It can be avoided and linted against by parties that find it a concern.That’s not decided per the proposal
It does introduce inconsistencies in the language if we start disallowing syntactic constructs just because we don’t like them. If we make
if erra special case and not treaterrortypes as false in all possible cases (at least inside anifstatement but ideally it should include all types of expressions) it makes the proposal even worse. That would make whole thing confusing and unintuitive. Image someone just starting to learn the language. How would you justify it? Right now the reasoning is “some people didn’t like how it looks”. If that’s the price to save a few strokes then I vote against such proposal. Either implement it properly or don’t implement it at all.@creker , honestly I would have just thought of an empty and nil slice to both be
false, if I were to use truthy testing instead of explicitely checking a length or comparing tonil. The language lets you calllenon a nil slice to get a length of 0 anyways, so one could say it already allows nil and empty slice to be treated the same. If the truthy behaviour is defined then its just something to know about the language.