discussion: spec: reduce error handling boilerplate using ? #71460
Replies: 56 comments 259 replies
-
Thumbs-up this thread if you think that it's worth considering a language change for a simpler error-handling syntax that reduces boilerplate. Thumbs-down this thread if you think that the current syntax is good enough, and that it's not worth changing the language. Note that this is only asking about syntax, not about other error-handling issues such as accidentally ignoring errors. Please don't use this thread to discuss syntax changes other than the ones in this proposal. This discussion is about a specific proposal; please do not start discussing the hundreds of other error handling proposals. |
Beta Was this translation helpful? Give feedback.
-
Thumbs-up this thread if you think the proposed implicit declaration of an Thumbs-down this thread if you think that implicit Use the confused emoji (😕 ) if you think that neither implicit |
Beta Was this translation helpful? Give feedback.
-
Thumbs-up this thread if you think it's OK to have Thumbs-down this thread if you think it's OK to have |
Beta Was this translation helpful? Give feedback.
-
Thumbs-up this thread if you think it's OK for the optional block to fall through to the code that follows. Thumbs-down this thread if you think that it should be an error if the optional block does not end with explicit flow control, such as |
Beta Was this translation helpful? Give feedback.
-
The conversion in this file doesn't seem right to me. https://go-review.googlesource.com/c/go/+/644076/1/src/cmd/go/internal/imports/scan.go#31
to
|
Beta Was this translation helpful? Give feedback.
-
In my view disadvantages 1,2,4, and 6 make for a compelling case that we would be sacrificing a lot of code clarity here. Whereas one of the go proverbs I go back to frequently as a guiding light is:
|
Beta Was this translation helpful? Give feedback.
This comment was marked as disruptive content.
This comment was marked as disruptive content.
This comment has been hidden.
This comment has been hidden.
-
Is it possible for us to update the cover to achieve half test coverage for one line of code execution? For example: Only executed one of the cases where err is nil or not nil. Display half covered of this line. |
Beta Was this translation helpful? Give feedback.
-
I'm not in favor of a language change like this as it hurts the readability of the code. As a vision impaired person, I would actually find it difficult to read code like this myself. I would, however, be open to other forms of syntax that are more readable like using a keyword like |
Beta Was this translation helpful? Give feedback.
-
How does the proposed // Because returning errors last is a convention, not part of the language...
func notIdiomatic() (error, string) {
return errors.New("yikes"), ""
} I'm assuming you have to return an error as the final value to opt in to this? |
Beta Was this translation helpful? Give feedback.
-
I'm new to this discussion so sorry if already discussed but Why not generalize ? to be an operator usable everywhere and not specific to Error ?
means remove last value from A and store it in a new variable |
Beta Was this translation helpful? Give feedback.
-
Is there a good way to think about "reading" the new syntax? I think it'd be easier to get used to it if I could substitute in a specific natural language phrase when I saw a question mark, so when I read it aloud I could follow the flow. Maybe "on error"? Not sure how that works with the I'm thinking specifically about trying to explain my code to someone newer to go, if they were to see this:
and ask "what does the |
Beta Was this translation helpful? Give feedback.
-
Hey @ianlancetaylor, I think this proposal is a really interesting step towards simplifying error handling in Go. One thing I was wondering about is how the implicit That said, I do think the potential to streamline error handling without interrupting code flow is appealing. The fewer lines of boilerplate, the better, especially for readability. I’d love to see how this might play out!!! |
Beta Was this translation helpful? Give feedback.
-
I believe it's too magical, we should keep it explicit and prohibit the use of
It looks more like Go code that we use to read and keep the encouragement to add an annotation. |
Beta Was this translation helpful? Give feedback.
-
I would prefer more explicit and more versatile though: r, err := SomeFunction() ? err != nil {
return fmt.Errorf("something failed: %w", err)
}
r, ok := SomeFunction() ? !ok {
return errors.New("I'm not ok when it is not ok")
} |
Beta Was this translation helpful? Give feedback.
-
Although I am still strongly opposed to the optional block (meaning that I believe the block should be required and explicit even in the simple The issue is that error handling is now going to be easy to omit by accident both by the writer of the code, and by the reviewer who reads the code as it is a single character. // oops I forgot to handle the error
Foo()
// Handled!
Foo() ? I believe that functions that return errors (at least as their last argument) must be explicitly vetted to either use the // Fails Vet
Foo()
// Passes Vet
_ = Foo()
err := Foo()
err = Foo()
Foo() ? This is a useful vet check today, but the problem is exacerbated if error handling is one trailing character that might be hidden off screen. |
Beta Was this translation helpful? Give feedback.
-
Very much a drive-by comment. I've quickly scanned the responses thus far for the term "scope" and I don't think this point is covered. So here goes.
Is In which case my brain is struggling, because it seems the above as being very similar to:
or
Maybe it's only me, but it doesn't seem visually consistent with similar constructs. |
Beta Was this translation helpful? Give feedback.
This comment was marked as off-topic.
This comment was marked as off-topic.
-
Someone who chooses the Go programming language does so purely for its philosophy of simplicity and efficiency. Otherwise, other languages remain a question mark, and the project or service intended to be written in Go would have been written in those languages instead. Do not sacrifice the language’s simplicity and philosophy for the sake of reducing token usage. |
Beta Was this translation helpful? Give feedback.
-
I find the '?' proposal a little too magical, and I'm Less space devoted to error handling could be Thus I would rather see a "return when" form:
We could even have a short form of the "return () when" form like
that is equivalent to today's ubiqitous
|
Beta Was this translation helpful? Give feedback.
-
In my opinion, this change is unnecessary - error handling in Go is both the worst and the best part of the language, but there is no point in changing it. much more useful would be the operator
is this readable? It's not for me to judge, well, but in any case it simplifies the code, because without it you would need an additional variable or anonymous function, and in such cases sometimes error handling is unnecessary |
Beta Was this translation helpful? Give feedback.
-
I genuinely do not see the advantage. It's only couple characters extra to not do this. Not doing this avoids treating errors as something special/magical other than a value. If the extra line is what bugs you, you can, today simply start doing: r, err := SomeFunction(); if err != nil {
return fmt.Errorf("something failed: %v", err)
} |
Beta Was this translation helpful? Give feedback.
-
In my humble opinion, I don't see much less verbosity. On the other hand, fundamental design principles of go are broken, such as the return of two variables. I find the third example interesting: If |
Beta Was this translation helpful? Give feedback.
-
Please, stop trying to change Go's error handling. Yes, it's a bit explicit. But it's so great that errors are just values. |
Beta Was this translation helpful? Give feedback.
-
What about our future ternary operator? 😅 (No please, I want this much more than ternary). Why is there a space before the "?" I vote no space from |
Beta Was this translation helpful? Give feedback.
-
@ianlancetaylor — While there are glimmers of a better future in this proposal, there are numerous issues I see with it. I have commented elsewhere on the things but I want to address my personal #1 concern; the fact that it would bake early return into Go as the easy path when early return is not how I want to handle errors. So let me propose a group of features to augment the proposal, in order of most important to me:
func doSomething() (int,error) {
a, @err := doSomethingA() ? goto err
b, @err := doSomethingB() ? goto err
c, @err := doSomethingC() ? goto err
value := handleIt(a,b,c)
err: {
err = fmt.Error("Something failed; %w",err)
}
return value,err
} If your proposal gets accepted, please please consider giving us the flexibility to handle errors with more than just an early return. Adding |
Beta Was this translation helpful? Give feedback.
-
I like explicit handling by default and would argue that Go "feels" simple because of how little implicit "magic" there is the language. That said, with this specific proposal I am concerned by:
(1) could be avoided (I think) by making the block non-optional. Either an empty block Combined, you might end up with something like: v := SomeFunction() ? nil // or {}
v := SomeFunction() ? myErrVar {
return DoSomethingWithError(myErrVar);
} (3) can't really be avoided if insisting on this shorthand // does not follow val, err convention
myErrVar, a, b, c := SomeFunction() ? myErrVar { // declared error variable has to shadow a declaration on the LHS
return DoSomethingWithError(myErrVar);
} |
Beta Was this translation helpful? Give feedback.
This comment has been minimized.
This comment has been minimized.
-
Being explicit is more effort once, but benefits hundreds down the road reading foreign code. Removing the repeating elements of error handling to get a more compact code structure, with less noise, while staying explicit, is a good idea. However, I don't like the value, err := doSomething() // no error handling here
value, err := doSomething() {} // return err, or panic, or ...
value, err := doSomething() {return errors.Join(err, fmt.Errorf("doSomething failed"))} // handle error
|
Beta Was this translation helpful? Give feedback.
-
This is a discussion about a new syntax that may be used to handle errors. This is issue #71203 converted into a discussion.
I've written a tool that converts ordinary Go code into code that uses the syntax from the proposal at #71203. That tool is available at https://go.dev/cl/643996. In order to build and use it you must first apply the changes in the patch series ending at https://go.dev/cl/642502.
Using that tool, I've converted much of the standard library to the new syntax. This can be seen at https://go.dev/cl/644076.
I encourage people interested in this proposal to take a look at these changes. Please consider whether the new code is more or less clear to the reader. Please consider whether the code logic stands out more clearly when there is less syntax devoted to error handling.
Most importantly, please avoid preconceptions when looking at this code. I'm not insisting that this change is better. But if you look at the changed code having already decided that this proposal is a bad idea, we won't learn anything. Really try to see whether the new code is better or worse.
If you choose to comment on how this proposal affects the standard library, please show specific examples to demonstrate your point. Don't argue in the abstract; look at real uses of real code.
The rest of this comment is largely a copy of #71203, partly updated, with some parts omitted.
Background
See #71203 for more background.
The goal of this proposal is to introduce a new syntax that reduces the amount of code required to check errors in the normal case, without obscuring flow of control.
New syntax
This section is an informal description of the proposal, with examples. A more precise description appears below.
I propose permitting statements of the form
to be written as
The
?
absorbs the error result of the function. It introduces a new block, which is executed if the error result is notnil
. Within the new block, the identifiererr
refers to the absorbed error result.Similarly, statements of the form
may be written as
Further, I propose that the block following the
?
is optional. If the block is omitted, it acts as though there were a block that simply returns the error from the function. For example, code likemay in many cases be written as
SomeFunction2() ?
Formal proposal
This section presents the formal proposal.
An assignment or expression statement may be followed by a question mark (
?
). The question mark is a new syntactic element, the first permitted use of?
in Go outside of string and character constants. The?
causes conditional execution similar to anif
statement. A?
at the end of a line causes a semicolon to be automatically inserted after it.A
?
uses a value as described below, referred to here as the qvalue.For a
?
after an assignment statement, the qvalue is the last of the values produced by the right hand side of the assignment. The number of variables on the left hand side of the assignment must be one less than the number of values produced by the right hand side (the right hand side values may come from a function call as usual). It is not valid to use a?
if there is only one value on the right hand side of the assignment.For a
?
after an expression statement the qvalue is the last of the values of the expression. It is not valid to use a?
after an expression statement that has no values.The qvalue must be of interface type and must implement the predeclared type
error
; that is, it must have the methodError() string
. In most cases it will simply be of typeerror
.A
?
is optionally followed by a block. The block may be omitted if the statement using?
appears in the body of a function, and the enclosing function has at least one result, and the qvalue is assignable to the last result (this means that the type of the last result must implement the predeclared typeerror
, and will often simply beerror
).Execution of the
?
depends on the qvalue. If the qvalue isnil
, execution proceeds as normal, skipping over the block if there is one.If the
?
is not followed by a block, and the qvalue is notnil
, then the function returns immediately. The qvalue is assigned to the final result. If the other results (if any) are named, they retain their current values. If they are not named, they are set to the zero value of their type. The results are then returned. Deferred functions are executed as usual.If the
?
is followed by a block, and the qvalue is notnil
, then the block is executed. Within the block a new variableerr
is implicitly declared, possibly shadowing other variables namederr
. The value and type of thiserr
variable will be those of the qvalue.That completes the proposal.
Discussion
This new syntax is partly inspired by Rust's question mark operator, though Rust permits
?
to appear in the middle of an expression and does not support the optional block. Also, I am suggesting that gofmt will enforce a space before the?
, which doesn't seem to be how Rust is normally written.Absorbing the error returned by a function, and optionally returning automatically if the error is not
nil
, is similar to the earlier try proposal. However, it differs in that?
is an explicit syntactic element, not a call to a predeclared function, and?
may only appear at the end of the statement, not in the middle of an expression.Declaring the err variable
As discussed above, when a block follows the
?
it implicitly declares a newerr
variable. There are no other cases in Go where we implicitly declare a new variable in a scope. Despite that fact, I believe this is the right compromise to maintain readability while reducing boilerplate.A common suggestion among early readers of this proposal is to declare the variable explicitly, for example by writing
In practice, though, the variable would essentially always be simply
err
. This would just become additional boilerplate. Since the main goal of this proposal is to reduce boilerplate, I believe that we should try our best to do just that, and introduceerr
in the scope rather than requiring people to declare it explicitly.If the implicit declaration of
err
seems too problematic, another approach would be to introduce a new predeclared name. The nameerr
would not be appropriate here, as that would be too often shadowed in existing code. However, a name likeerrval
orerv
would work. Within a?
optional block, this name would evaluate to the qvalue. Outside of a?
optional block, referring to the name would be a compilation error. This would have some similarities to the predeclared nameiota
, which is only valid within aconst
declaration.A third approach would be for
errval
orerv
to be a predeclared function that returns the qvalue.Supporting other types
As discussed above the qvalue must be an interface type that implements
error
. It would be possible to support other interface types. However, the?
operator, and especially the implicitly declarederr
variable, is specifically for error handling. Supporting other types confuses that focus. Using?
with non-error
types would also be confusing for the reader. Keeping a focus on just handling errors seems best.It would also be possible to support non-interface types that implement
error
, such as the standard library type*os.SyscallError
. However, returning a value of that type from a function that returnserror
would mean that the function always returns a non-nil error value, as discussed in the FAQ. Using different rules for?
would make an already-confusing case even more confusing.Effects on standard library
See https://go.dev/cl/644076.
The latest version of the conversion tool found 723,292 statements in the standard library. It was able to convert 14,304 of them to use
?
. In all, 1.98% of all statements were changed. 2,825 statements, or 0.39% of the total, were changed to use a?
with no optional block.In other words, adopting this change across the ecosystem would touch an enormous number of lines of existing Go code. Of course, changing existing code could happen over time, or be skipped entirely, as current code would continue to work just fine.
Pros and cons
Pros
Advantage 1: Rewriting
to
reduces the error handling boilerplate from 9 tokens to 5, 24 non-whitespace characters to 12, and 3 boilerplate lines to 2.
Rewriting
to
reduces boilerplate from 9 tokens to 1, 24 non-whitespace characters to 1, and 3 boilerplate lines to 0.
Advantage 2: This change turns the main code flow into a straight line, with no intrusive
if err != nil
statements and no obscuringif v, err = F() { … }
statements. All error handling either disappears or is indented into a block.Advantage 3: That said, when a block is used the
}
remains on a line by itself, unindented, as a signal that something is happening. (I'm also listing this as a disadvantage, below.)Advantage 4: Unlike the try proposal and some other error handling proposals, there is no hidden control flow. The control flow is called out by an explicit
?
operator that can't be in the middle of an expression, though admittedly the operator is small and perhaps easy to miss at the end of the line. I hope the blank before it will make it more visible.Advantage 5: To some extent this reduces a couple of common error handling patterns to just one, as there is no need to decide between
and
Instead people can consistently write
Cons
Disadvantage 1: This is unlike existing languages, which may make it harder for novices to understand. As noted above it is similar to the Rust
?
operator, but still different. However, it may not be too bad: Todd Kulesza did a user study and discovered that people unfamiliar with the syntax were able to see that the code had to do with error handling.Disadvantage 2: The shadowing of any existing
err
variable may be confusing. Here is an example from the standard library where the?
operator can not be easily used:fmt/scan.go:
In this example the assignment
err = nil
has to change theerr
variable that exists outside of thefor
loop. Using the?
operator would introduce a newerr
variable shadowing the outer one. (In this example using the?
operator would cause a compiler error, because the assignmenterr = nil
would set a variable that is never used.)Disadvantage 3: When using a block, the
}
remains on a line itself, taking up space as pure boilerplate. (I'm also listing this as an advantage, above.)Disadvantage 4: No other block in Go is optional. The semicolon insertion rule, and the fact that a block is permitted where a statement is permitted, means that inserting or removing a newline can convert one valid Go program into another. As far as I know, that is not true today.
For example, these two functions would both be valid and have different meanings, although the only difference is whitespace.
Disadvantage 5: For an expression statement that just calls a function that returns an error, it's easy to accidentally forget the
?
and writeF()
rather thanF() ?
. Of course it's already easy to forget to check the error result, but once people become accustomed to this proposal it may be easy to overlook the missing?
when reading code.Disadvantage 6: This proposal has no support for chaining function calls, as in
F().G().H()
, whereF
andG
also have an error result.Disadvantage 7: This proposal makes it easier to simply return an error than to annotate the error, by using a plain
?
with no block. This may encourage programmers to skip error annotations even when they are desirable.Disadvantage 8: We really only get one chance to change error handling syntax in Go. We aren't going to make a second change that touches 1.5% of the lines of existing Go code. Is this proposal the best that we can do?
Disadvantage 9: We don't actually have to make any changes to error handling. Although it is a common complaint about Go, it's clear that Go is usable today. Perhaps no change is better than this change. Perhaps no change is better than any change.
Transition
If we adopt this proposal, we should provide tools that can be used to automatically rewrite existing Go code into the new syntax. Not everyone will want to run such a tool, but many people will. Using such a tool will encourage Go code to continue to look the same in different projects, rather than taking different approaches. This tool can't be gofmt, as correct handling requires type checking which gofmt does not do. It could be an updated version of
go fix
. See also modernizers.We will have to update the go/ast package to support the use of
?
, and we will have to update all packages that use go/ast to support the new syntax. That is a lot of packages.We will also have to update the introductory documentation and the tour. And, of course, existing Go books will be out of date and will need updating by their authors. The change to the language and compiler is the easiest part of the work.
Beta Was this translation helpful? Give feedback.
All reactions