-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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: spec: reduce error handling boilerplate using ? #71203
Comments
If, instead of a block, it took an optional value of type
which is still quite short |
Would |
This comment was marked as off-topic.
This comment was marked as off-topic.
No. The |
@ianlancetaylor Is there a rationale for that that I missed? |
Personally, I don't feel like the first version (with the block) pays for itself. The visual difference between Also, a nit: I find the formatting with the extra space between the rvalue and the |
stmt ? err => {
return err
} I think that's a little confusing because that |
No, I didn't write anything about that topic. The rationale is that I don't think a return should occur in the middle of an expression. I think that is harder to understand, especially as we heard toward cases like |
I think that's fair for the calls, but what about the |
Introducing a special syntax solely for the undecorated (in many cases discouraged) error return might make it appear disproportionately more appealing than the preferred, decorated method of handling errors. |
@kortschak I suppose I'm trying to keep it as simple as possible, and also I want to ensure that it's always reasonably easy to return an annotated error, rather than biasing even more heavily in favor of an unannotated error (as @gophun also mentions). |
Yeah, I'm just trying to scope out what the intention is here. If it's just assignment, it doesn't really seem worth it to me. |
Issue #21161 says,
The current proposal helps a great deal with this. I would like to describe a possible extension. The idea isn't to hijack the current discussion -- this can certainly be added at a later time. I just want to show how this goal can be furthered. It is exceedingly common for the error handler to consist only in a return statement. The "..." proposal #21182 is meant to declutter that statement. But I feel like we have the opportunty to bring these ideas together and make all of The extension is to allow ? to be followed by what amounts to arguments to fmt.Errorf, with return ... (in the sense #21182) of inserted before them. For example, eight lines in debug/elf/file.go:
Can become:
Six lines in net/http/transport_test.go become:
(They would also have to have a block, and The main disadvantage of this extension is that it adds a dependency on package fmt in code that uses it, and long lines will probably have to be discussed as well. If the current issue is accepted I'll file a separate proposal. |
What is the order of assignment, and are any hidden temporary variables used when handling a non-nil error? That is, would this:
output 0 or 1? Or, if dereferencing a pointer during the assignment:
would this create a temporary variable for the first return value and avoid the nil dereference when it takes the error path? |
@jrick Good questions. I think that when there is a block, the assignment should complete before the block is executed. When there is no block, the assignment should not be done. Those seems likes the least confusing choices. |
My only concern there is that by refactoring and adding additional context/wrapping to the error in an error block would now perform the nil dereference and crash where it would not have before. |
@jrick That is true, but it seems to me that if the block refers to the variable on the left hand side of the assignment, it should see the new value, not the old one. Especially since if the assignment uses |
Yes, that makes sense. I thought about perhaps using some trickery where only the assignments that were used by the error block were performed prior to the block running, and all other assignments later, but I don't think it is really possible to perform properly due to the potential of pointer aliasing. |
That was not my intent. It would be an ordinary function that is called in the ordinary manner. It's return would be used as the value of the error. x, err := stmt
if err != nil {
return handler(err)
} |
@jimmyfrasche I see. But then, is there a way to use |
The handler let's you transform the error (for example, adding extra context or performing clean up actions). What happens after that is up to If you want if err != nil {
panic(handler(err))
} Personally I think
|
@jimmyfrasche's suggestion would also solve the problem of package fmt
func Handlef(fmt string, args ...any) func(error) error {
return func(err error) error {
// Maybe something a bit fancier to allow the error to be anywhere in the format string.
args = append(args, err)
return fmt.Errorf(fmt, args...)
}
} package example
func Example() error {
Start() ? fmt.Handlef("start failed: %w")
Run() ? fmt.Handlef("run failed: %w")
return nil
} It's a much less complicated version of #69734. |
For me it pays fully, because it is not only about token reduction like many make it to be but the main problem many have with the current error handling is the interleaving of the main flow with the error flow that makes reading, not writing, harder. |
@doggedOwl I wasn't talking about token-reduction - on the contrary. To me, both forms simply look almost the same, despite one having fewer tokens. So I agree that token-reduction isn't a good metric. |
In my impression, in many similar proposals, the use of |
I personally like this proposal because too many err != nil indeed affects the readability of control flow. func Example() (string, error, error) {
if somecase {
return "yes", nil, nil
} else {
return "no", fmt.Errorf("err1"), fmt.Errorf("err2")
}
} And these two errors can be two different custom errors, so how does |
r := SomeFunction() ? err {
return fmt.Errorf("something failed: %v", err)
} Like many others, I believe it's better to explicitly declare variable names. However, r, err := SomeFunction() ?
^~~ receive an error as usual r, err := SomeFunction() ? {
return nil, fmt.Errorf("something failed: %v", err)
} seems slightly better than |
@luckycatx The proposal would use the last |
example that was given archive/zip/reader.go: p, err := findDirectory64End(r, directoryEndOffset)
if err == nil && p >= 0 {
directoryEndOffset = p
err = readDirectory64End(r, p, d)
}
err ? why then no go step further and allow this if function returns error p, ? := findDirectory64End(r, directoryEndOffset)
if err == nil && p >= 0 {
directoryEndOffset = p
err = readDirectory64End(r, p, d)
} |
Proposal Details
Background
As discussed in the introduction to an earlier, declined, proposal, Go programs have a lot of error checking code. In surveys error handling is listed as the biggest specific challenge people face using Go today.
There have been many proposals to address this, summarized in a meta-proposal.
This is yet another such proposal. This proposal has some similarities to onerr return, add "or err: statement" after function calls for error handling, and use ? simplify handling of multiple-return-values. It is in some ways a reworking of simplify error handling with || err suffix. There are probably a number of other proposals that fed into this one even if I can't remember them now.
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.
Examples
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
I applied a simple rewriter to the standard library to introduce uses of
?
where feasible. Here are some examples of new code:archive/tar/common.go:
archive/tar/writer_test.go:
archive/zip/reader.go:
archive/zip/reader.go:
archive/zip/reader_test.go:
cmd/cgo/godefs.go:
cmd/cgo/out.go:
os/exec/exec.go:
The conversion tool found 544,294 statements in the standard library. It was able to convert 8820 of them to use
?
. In all, 1.6% of all statements were changed. 1380 statements, or 0.25% 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.
Possible extensions
These are some possible extensions to the above proposal. These are not part of this proposal, but suggest ways that the language could be developed if this proposal seems useful.
Permit return …, err
This proposal works well with proposal #21182 which permits
return …, err
to return all zero values other than a final error value. For example,That said, if we know that
err != nil
, then writingerr ?
is the same as writingreturn …, err
(ignoring the details of named result parameters). We could useerr ?
in the above example. It would require some analysis to see how oftenreturn …, err
would be useful when we don't know whether or noterr
isnil
.Permit using
?
with no block outside of a functionWe could permit using
?
with no block outside of a function, by having it implicitly callpanic(err)
. For example:I don't think this comes up often enough to be worth doing. Note that this proposal permits
Permit using
?
with no block in a testWe could permit using
?
with no block in a test function, by having it implicitly callt.Fatal(err)
. For example:I'm not fond of this because it means that we have to somehow recognize test functions in the language. This proposal does already permit
or, for that matter,
Let gofmt retain single line
We could let gofmt retain a
?
block on a single line, as inThis would reduce the error handling boilerplate by 2 newlines, and keep all error handling indented.
I am not in favor of this myself but I know that people will suggest it.
The text was updated successfully, but these errors were encountered: