discussion: spec: reduce error handling boilerplate using ? #71460
Replies: 89 comments 468 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.
This comment has been minimized.
This comment has been minimized.
-
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.
-
What about a reduced proposal for just that ?
It let us improve it later to allow |
Beta Was this translation helpful? Give feedback.
This comment has been minimized.
This comment has been minimized.
-
OK, this is a very interesting post, but it lacks explicit declarations: all returned values should be specified. Here is a step-by-step example with explicit declarations. First, remove func whatever() (*int, MyStruct, error) {
err1 := getError() ? {
return nil, MyStruct{}, err1 // return err1
}
i, err2 := getIntAndError() ? {
return nil, MyStruct{}, err2 // return err2
}
j, err3 := getIntAndError() ? {
return nil, MyStruct{}, fmt.Errorf("wrap: %w", err3) // return wrapped err3
}
return &i, MyStruct{Foo: j}, nil
} Then, same example, without the return blocks for a more compact writing: func whatever() (*int, MyStruct, error) {
err1 := getError() ? nil, MyStruct{}, err1 // return err1
i, err2 := getIntAndError() ? nil, MyStruct{}, err2 // return err2
j, err3 := getIntAndError() ? nil, MyStruct{}, fmt.Errorf("wrap: %w", err3) // return wrapped err3
return &i, MyStruct{Foo: j}, nil
} At last, same example but with a new optional helper (like Note: all errors should be declared and used but for Mentally replace the content of the helper when you read a func whatever() (*int, MyStruct, error) {
? (err error) { // declaration of some kind of a "catch" method
return nil, MyStruct{}, err
}
getError() ? // catch & return err
i, err2 ? := getIntAndError() // catch & return err2 (err2 is declared and used only here)
j, err3 := getIntAndError() ? fmt.Errorf("wrap: %w", err3) // direct wrap err3, catch & return wrapped err3
k, err4 := getIntAndError() ? {
return fmt.Errorf("with block: %w", err4) // wrap err4 in a block, catch & return wrapped err4
}
return &i, MyStruct{Foo: j, Bar: k}, nil
} To sum up
I took a lot of shortuts, so now, it is your turn to find a realistic compliant solution ! // optional use of a "catch" method to fill returned values
? (err error) {
return nil, MyStruct{}, err
}
// use of `?` at declaration
i, err ? := getIntAndError()
// use with an additional instruction or block
i, err := getIntAndError() ? panic(err)
// declare and use
i, err := getIntAndError()
err ? // like: return err
err ? fmt.Errorf("wrap: %w", err) // return wrapped err
err ? { // or with block
panic(err)
}
// should not compile due to lack of error declaration
i, _ := getIntAndError() ? |
Beta Was this translation helpful? Give feedback.
-
I liked the slide Rob had in his 2015 talk "Simplicity is Complicated".
The "boilerplate" you're referring to is a core feature of Go. It's explicit, simple, and somewhat verbose. And that's okay! We don't need "new syntax partly inspired by language X or Y". I use "we" to refer to those who have been writing Go for more than weekend projects - those with production experience or substantial oss contributions. While you've reduced it to a one-liner, I've come to really appreciate the explicit nature of error handling (forcing developers to think about how to handle errors at each call site). The This proposal has limited merit - it adds another way to do things and makes code harder to read (ironically). I really hope Google is not on a path to grow the language at all costs, as that would undermine what makes Go lovable for its long-term users. Let's keep Go simple and boring. |
Beta Was this translation helpful? Give feedback.
-
Here is my 2 word on this proposal, As a user and a fan of GO, I was attracted and fell in love with the simplicity of the GO. I think we do not need to update the error handling, and it is fine as it is. |
Beta Was this translation helpful? Give feedback.
-
I'm a Go user with almost 10 years of experience, and I've never thought that error handling is a problem. It's simple—let's keep the language simple and not allow doing the same thing in many different ways. I think there are many other areas where Go can be improved, but at least not in its syntax and error handling. |
Beta Was this translation helpful? Give feedback.
-
My main issue with this proposal is that it tries to make returning an unwrapped error easier, and only slightly improves the ergonomics of returning wrapped errors. I would be much more interested in a proposal which focuses on making wrapping errors easier. |
Beta Was this translation helpful? Give feedback.
-
I took a look through the converted std lib with my best attempt at an open mind. I found the converted code to be substantially less clear overall. Lack of leading I also really dislike the new ambiguity around the number of returned values from functions. For context, I am a big fan of Go generics and am very happy to have them in the language, so this is not coming from a perspective of being against all new additions. |
Beta Was this translation helpful? Give feedback.
-
I think it's okay to just do this. value, err ?= sumFunc() {
return err
} The block will execute when the error is not nil. The I don't mind My idea doesn't extend to Another idea is to just make a special block catch value, err := sumFunc() {
return err
} The |
Beta Was this translation helpful? Give feedback.
This comment has been minimized.
This comment has been minimized.
-
I'm glad that the Go team is addressing error handling in Go and striving for improvement. I like the current error handling in Go, but having worked on many projects and teams where people mostly came from other languages like Java or PHP, error handling in Go was terrible because there was Could you simplify it like this?
|
Beta Was this translation helpful? Give feedback.
-
Time to think outside of the box! For simply returning value, err? := something() // return err
err?, value := something() // ... would obv. work as well! position doesn't matter, easy to parse For handling value, err := something()
err? {
// do something with err
} or inline (i hate this) value, err ? { return fmt.Errorf(...) } := something() Or if value, guard err := something() // `guard` as new keyword value, err := something()
guard err {
// do something with err
} |
Beta Was this translation helpful? Give feedback.
-
One more suggestion without
in case if we want to skip error, which now equal
it's kind of we just moved err variable from a long syntax and mixed with short syntax, kind of successor
Become
|
Beta Was this translation helpful? Give feedback.
-
Part of the appeal of Golang for me is the simplicity and verbosity of the control flow. This change while making writing slightly more convenient (less symbols to type), makes the language more complex and readability harder. If I want error handling where an error can bubble up seamlessly up the call stack, I could just go with a language that supports exceptions. I really don't see why people want to turn Golang into a language it isn't or rather wasn't. |
Beta Was this translation helpful? Give feedback.
-
I like @dmitryuck 's idea. It's good but I think it should require a new keyword. Instead of this. r := SomeFunction() err {
return fmt.Errorf("something failed: %v", err)
} We need this. r := catch SomeFunction() err {
return fmt.Errorf("something failed: %v", err)
} People need to know that the block that is being used is special. When the block returns that means the function returns too. The problem with your proposal is that you talked about there being an implicit variable with a specific name. It's best to just let the variable be the name the developer chooses. |
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