discussion: spec: reduce error handling boilerplate using ? #71460
Replies: 112 comments 605 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.
-
I really like the given proposal. However, I feel like it tries to achieve too much at the same time. What do you think if we differentiate between error context wrapping and full error handling? This would allow us to focus on simplifying the most common case: adding context during error propagation. I think a dedicated keyword like // fmt.Errorf("something else failed: %v", err)
r := SomeFunction() retwerr "something failed: %v"
// fmt.Errorf("something else failed: %v", err)
SomeFunction2() retwerr "something else failed: %v"
// err
SomeFunction2() reterr In this case, there is no way to directly access the Grey areas:
|
Beta Was this translation helpful? Give feedback.
-
If I understand the proposal correct. You also can't use the proposal to assign fields inside a struct. E.g. the following would not be allowed: func formatX(somepkg.X) (string, error)
func formatY(somepkg.Y) (string, error)
func ToDestStruct(src somepkg.SrcStruct) (DestStruct, error) {
var errs []error
dest := DestStruct{
X: formatX(src.X) ? { errs = append(errs, fmt.Errorf("x: %w")) }, // not allowed
Y: formatY(src.Y) ? { errs = append(errs, fmt.Errorf("y: %w")) }, // not allowed
}
if err := errors.Join(errs...); err != nil {
return DestStruct{}, err
}
return r, nil
} We have a lot of code that implement some variation of this in our own code base. I personally feel that if the syntax was allowed inside struct initialization, it would significantly increase the value of the proposal. Real examaple: Below is a slightly modified example from our own code-base: // scatterPlot describe the API layer transport model for the scatter plot type.
type scatterPlot struct {
Type string `json:"type"`
Color string `json:"color"`
Opacity int `json:"opacity"`
X scatterPlotAxis `json:"x"`
Y scatterPlotAxis `json:"y"`
Size *scatterPlotAxis `json:"size"`
Hue *scatterPlotAxis `json:"hue"`
}
// toBusinessScatterPlot validates and converts a scatter plot transport model to
// it's business layer representation. Validation errors are aggregated into a
// [bend.PathErrors] instance.
func toBusinessScatterPlot(v scatterPlot) (visfield.ScatterPlot, error) {
eb := bend.NewErrorBuilder()
color, err := textfield.ParseColor(v.Color)
if err != nil {
eb.Field("color").Error(err)
}
opacity, err := visfield.ParseOpacity(v.Opacity)
if err != nil {
eb.Field("opacity").Error(err)
}
x, err := parseScatterPlotAxis(v.X)
if err != nil {
eb.Field("x").Error(err)
}
y, err := parseScatterPlotAxis(v.Y)
if err != nil {
eb.Field("y").Error(err)
}
size, err := parseScatterPlotAxisOptional(v.Size)
if err != nil {
eb.Field("size").Error(err)
}
hue, err := parseScatterPlotAxisOptional(v.Hue)
if err != nil {
eb.Field("hue").Error(err)
}
if err := eb.BuildError(); err != nil {
return visfield.ScatterPlot{}, err
}
return visfield.ScatterPlot{
Color: color,
Opacity: opacity,
X: x,
Y: y,
Size: size,
Hue: hue,
}, nil
} How this could look like with the described proposal, if it was allowed: func toBusinessScatterPlot(v scatterPlot) (visfield.ScatterPlot, error) {
eb := bend.NewErrorBuilder()
v2 := visfield.ScatterPlot{
Color: textfield.ParseColor(v.Color) ? { eb.Field("color").Error(err) },
Opacity: visfield.ParseOpacity(v.Opacity) ? { eb.Field("opacity").Error(err) },
X: parseScatterPlotAxis(v.X) ? { eb.Field("x").Error(err) },
Y: parseScatterPlotAxis(v.Y) ? { eb.Field("y").Error(err) },
Size: parseScatterPlotAxisOptional(v.Size) ? { eb.Field("size").Error(err) },
Hue: parseScatterPlotAxisOptional(v.Hue) ? { eb.Field("hue").Error(err) },
}
if err := eb.BuildError(); err != nil {
return visfield.ScatterPlot{}, err
}
return v2, nil
} |
Beta Was this translation helpful? Give feedback.
This comment has been minimized.
This comment has been minimized.
-
I like the shorthand syntax when the error is trivial and I want instant propagation without distracting from the happy path: val := doSomething() ? But I'm not sure this syntax provides enough benefit over the traditional val := doSomething() ? {
return nil, fmt.Errorf("event x caused error: %w", err)
}
// vs
val, err := doSomething()
if err != nil {
return nil, fmt.Errorf("event x caused error: %w", err)
} my 2 cents |
Beta Was this translation helpful? Give feedback.
-
I am extremely opposed to this proposal, even if I am called a "stubborn person". Obviously, I can't change anything on my own, but I will always resist this way of writing. (If the ide recommends this way of writing to me, I guess I will throw it into the trash can) |
Beta Was this translation helpful? Give feedback.
-
While the ? syntax effectively reduces boilerplate, I'd like to propose an alternative that may improve readability and align better with Go’s existing conventions. This is also inspired by another language, and the verbosity is definitely taken else where. Instead of:
I propose:
Rationale: |
Beta Was this translation helpful? Give feedback.
-
The Go team does not have to ask for permission to implement the rewrite tool so that it changes code visualized, but not the actual code saved in the editor. If this really is wanted and disliked by large portions of the go ecosystem (seems to be almost 50/50) then maybe it makes sense to compromise and implement IDE visualization tools which do not affect the actual code. |
Beta Was this translation helpful? Give feedback.
-
I have played with the proposed compiler for some time and want to highlight a few concrete examples from the Go standard library. IMO, the proposal doesn't just reduce boilerplate—it also enables reusable patterns that improve skimmability and maintainability. For instance, the The updated version: func (p *addrParser) parseSingleAddress() (*Address, error) {
addrs := p.parseAddress(true) ?
expect(p.skipCFWS(), "mail: misformatted parenthetical comment") ?
expect(p.empty(), "mail: expected single address, got %q", p.s) ?
expect(len(addrs) != 0, "mail: empty group") ?
expect(len(addrs) <= 1, "mail: group with multiple addresses") ?
return addrs[0], nil
} This approach reduces visual noise while preserving explicit control flow, making it easier to scan for error conditions. But let's have a more suitable example, first, produced by the func (tw *Writer) AddFS(fsys fs.FS) error {
return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error {
err ?
if name == "." {
return nil
}
info := d.Info() ?
// TODO(#49580): Handle symlinks when fs.ReadLinkFS is available.
if !d.IsDir() && !info.Mode().IsRegular() {
return errors.New("tar: cannot add non-regular file")
}
h := FileInfoHeader(info, "") ?
h.Name = name
tw.WriteHeader(h) ?
if d.IsDir() {
return nil
}
f := fsys.Open(name) ?
defer f.Close()
_, err = io.Copy(tw, f)
return err
})
} It's much easier to skim than the original. But with the second version, we can do even more with the func (tw *Writer) AddFS(fsys fs.FS) error {
return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error {
err ?
expect(name != ".", nil) ?
info := d.Info() ?
// TODO(#49580): Handle symlinks when fs.ReadLinkFS is available.
expect(d.IsDir() || info.Mode().IsRegular(), "tar: cannot add non-regular file")
h := FileInfoHeader(info, "") ?
h.Name = name
tw.WriteHeader(h) ?
expect(!d.IsDir(), nil) ?
f := fsys.Open(name) ?
defer f.Close()
io.Copy(tw, f) ?
return nil
})
} Lessons learned while playing with ? After experimenting with For example, when a function ends with io.Copy(tw, f) ?
return nil
}) Interestingly, if you leave out return, the compiler doesn't throw an error—it just panics with: <unknown line number>: internal compiler error: panic: unreachable (FTR, I tested with the optional block, and the result was the same.) Now that we have a standard way to propagate errors, we can build on it.
|
Beta Was this translation helpful? Give feedback.
-
I would like to ask how long do you plan to keep this discussion open? It could take weeks or months, with people arguing about whether this is good or bad. What are the next steps regarding error handling? What happens if this proposal is accepted or rejected? I think each proposal should have an ETA or deadline so everyone is on the same page |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
1.single line, with error 2.return new error 3.multi line |
Beta Was this translation helpful? Give feedback.
-
It seems like this proposal has gotten way out of hand. People seem to have many different ideas for how error handling should be handled. Even though nothing is going to change anytime soon this discussion is something that people are passionate about. I to also wanted a different keyword to. But that's not going to happen. So far there are three main ideas regarding this proposal about what the
When it comes to idea number one it's name will be called the Error Capture operator. Immediate return. func doRequest() error {
doSomething()?
} Error subversion. func doRequest() error {
doSomething()? err { return fmt.ErrorF("Something's wrong %w", err) }
} Capturing the value on the left. func doRequest() error {
value := doSomething()?
} When it comes to idea number two I will call this idea the Map v := m["k"]? {
return fmt.Errorf("map key not found: %v", k)
} Type assertion s := i.(string)? {
return fmt.Errorf("type is not a string: %v", i)
} Channels v := <-c? {
return fmt.Errorf("channel has been closed")
} Function returning two values. value := doSomething() ? error { fmt.Errorf("Invalid %w" error) } The third idea is based on the idea that the function doRequest() (string, error) {
var error error;
doSomething()? goto catch;
doSomethingElse()? goto catch;
catch:
error = err
goto end
end:
return "", error
return "Value ", nil
} This idea can also apply to the second idea as well. This fourth idea would be called Zero Value Operator (GOTO). function doSomething() error {
v := m["k"]? goto createErr
createErr:
fmt.Errorf("map key not found")
} Well it's up to you guys to vote on which one you'd prefer. Please reply to this post on which idea you want. I won't argue for one over the other. This idea is a strange one to me. Vote!
|
Beta Was this translation helpful? Give feedback.
-
@ianlancetaylor Seems that most of the benefits would come from the simplest form
May I suggest to go small at the beginning, and just allow this simplest form? Once it is used here and there, one of the next versions may have some variations with more complex form if it would be even required, since classical |
Beta Was this translation helpful? Give feedback.
This comment has been minimized.
This comment has been minimized.
-
I like the idea and general sentiment of reducing boilerplate, and the proposal makes sense to me. Though I can't help but feel a '?' is not very visible and the meaning not immediately obvious. I'd prefer something more similar to the original try proposal. A new keyword like 'check' is immediately more visible and the intent is much clearer. Moving it from the end to the beginning is also better for readability. An optional block like in this proposal would also apply. v, err := do()
v := check do()
v := check do() {
return fmt.Errorf("some error: %w", err)
} It would be nice to support error variable names other than 'err', and it does feel a bit magical for it to just appear. I wonder if it would be possible to adjust this to explicitly declare error variables such: v := check do(); err {
return fmt.Errorf("some error: %w", err)
} I'd been to hear your thoughts. Sorry if this has already been proposed or discussed. |
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