try
This proposal has been closed. Thanks, everybody, for your input.
Before commenting, please read the detailed design doc and see the discussion summary as of June 6, the summary as of June 10, and _most importantly the advice on staying focussed_. Your question or suggestion may have already been answered or made. Thanks.
We propose a new built-in function called try
, designed specifically to eliminate the boilerplate if
statements typically associated with error handling in Go. No other language changes are suggested. We advocate using the existing defer
statement and standard library functions to help with augmenting or wrapping of errors. This minimal approach addresses most common scenarios while adding very little complexity to the language. The try
built-in is easy to explain, straightforward to implement, orthogonal to other language constructs, and fully backward-compatible. It also leaves open a path to extending the mechanism, should we wish to do so in the future.
[The text below has been edited to reflect the design doc more accurately.]
The try
built-in function takes a single expression as argument. The expression must evaluate to n+1 values (where n may be zero) where the last value must be of type error
. It returns the first n values (if any) if the (final) error argument is nil, otherwise it returns from the enclosing function with that error. For instance, code such as
f, err := os.Open(filename)
if err != nil {
return …, err // zero values for other results, if any
}
can be simplified to
f := try(os.Open(filename))
try
can only be used in a function which itself returns an error
result, and that result must be the last result parameter of the enclosing function.
This proposal reduces the original draft design presented at last year's GopherCon to its essence. If error augmentation or wrapping is desired there are two approaches: Stick with the tried-and-true if
statement, or, alternatively, “declare” an error handler with a defer
statement:
defer func() {
if err != nil { // no error may have occurred - check for it
err = … // wrap/augment error
}
}()
Here, err
is the name of the error result of the enclosing function. In practice, suitable helper functions will reduce the declaration of an error handler to a one-liner. For instance
defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
(where fmt.HandleErrorf
decorates *err
) reads well and can be implemented without the need for new language features.
The main drawback of this approach is that the error result parameter needs to be named, possibly leading to less pretty APIs. Ultimately this is a matter of style, and we believe we will adapt to expecting the new style, much as we adapted to not having semicolons.
In summary, try
may seem unusual at first, but it is simply syntactic sugar tailor-made for one specific task, error handling with less boilerplate, and to handle that task well enough. As such it fits nicely into the philosophy of Go. try
is not designed to address _all_ error handling situations; it is designed to handle the _most common_ case well, to keep the design simple and clear.
This proposal is strongly influenced by the feedback we have received so far. Specifically, it borrows ideas from:
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md
tryhard
tool for exploring impact of try
I agree this is the best way forward: fixing the most common issue with a simple design.
I don't want to bikeshed (feel free to postpone this conversation), but Rust went there and eventually settled with the ?
postfix operator rather than a builtin function, for increased readability.
The gophercon proposal cites ?
in the considered ideas and gives three reason why it was discarded: the first ("control flow transfers are as a general rule accompanied by keywords") and the third ("handlers are more naturally defined with a keyword, so checks should too") do not apply anymore. The second is stylistic: it says that, even if the postfix operator works better for chaining, it can still read worse in some cases like:
check io.Copy(w, check newReader(foo))
rather than:
io.Copy(w, newReader(foo)?)?
but now we would have:
try(io.Copy(w, try(newReader(foo))))
which I think it's clearly the worse of the three, as it's not even obvious anymore which is the main function being called.
So the gist of my comment is that all three reasons cited in the gophercon proposal for not using ?
do not apply to this try
proposal; ?
is concise, very readable, it does not obscure the statement structure (with its internal function call hierarchy), and it is chainable. It removes even more clutter from the view, while not obscuring the control flow more than the proposed try()
already does.
To clarify:
Does
func f() (n int, err error) {
n = 7
try(errors.New("x"))
// ...
}
return (0, "x") or (7, "x")? I'd assume the latter.
Does the error return have to be named in the case where there's no decoration or handling (like in an internal helper function)? I'd assume not.
Your example returns 7, errors.New("x")
. This should be clear in the full doc that will soon be submitted (https://golang.org/cl/180557).
The error result parameter does not need to be named in order to use try
. It only needs to be named if the function needs to refer to it in a deferred function or elsewhere.
I am really unhappy with a built-in _function_ affecting control flow of the caller. This is very unintuitive and a first for Go. I appreciate the impossibility of adding new keywords in Go 1, but working around that issue with magic built-in functions just seems wrong to me. It's worsened by the fact that built-ins can be shadowed, which drastically changes the way try(foo)
behaves. Shadowing of other built-ins doesn't have results as unpredictable as control flow changing. It makes reading snippets of code without all of the context much harder.
I don't like the way postfix ?
looks, but I think it still beats try()
. As such, I agree with @rasky .
Edit: Well, I managed to completely forget that panic exists and isn't a keyword.
The detailed proposal is now here (pending formatting improvements, to come shortly) and will hopefully answer a lot of questions.
@dominikh The detailed proposal discusses this at length, but please note that panic
and recover
are two built-ins that affect control flow as well.
One clarification / suggestion for improvement:
if the last argument supplied to try, of type error, is not nil, the enclosing function’s error result variable (...) is set to that non-nil error value before the enclosing function returns
Could this instead say is set to that non-nil error value and the enclosing function returns
? (s/before/and)
On first reading, before the enclosing function returns
seemed like it would _eventually_ set the error value at some point in the future right before the function returned - possibly in a later line. The correct interpretation is that try may cause the current function to return. That's a surprising behavior for the current language, so a clearer text would be welcomed.
I think this is just sugar, and a small number of vocal opponents teased golang about the repeated use of typing if err != nil ...
and someone took it seriously. I don't think it's a problem. The only missing things are these two built-ins:
Not sure why anyone ever would write a function like this but what would be the envisioned output for
try(foobar())
If foobar
returned (error, error)
I retract my previous concerns about control flow and I no longer suggest using ?
. I apologize for the knee-jerk response (though I'd like to point out this wouldn't have happened had the issue been filed _after_ the full proposal was available).
I disagree with the necessity for simplified error handling, but I'm sure that is a losing battle. try
as laid out in the proposal seems to be the least bad way of doing it.
@webermaster Only the last error
result is special for the expression passed to try
, as described in the proposal doc.
Like @dominikh, I also disagree with the necessity of simplified error handling.
It moves vertical complexity into horizontal complexity which is rarely a good idea.
If I absolutely had to choose between simplifying error handling proposals, though, this would be my preferred proposal.
It would be helpful if this could be accompanied (at some stage of accepted-ness) by a tool to transform Go code to use try
in some subset of error-returning functions where such a transformation can be easily performed without changing semantics. Three benefits occur to me:
try
could be used in their codebase.try
lands in a future version of Go, people will likely want to change their code to make use of it. Having a tool to automate the easy cases will help a lot.try
will make it easy to examine the effects of the implementation at scale. (Correctness, performance, and code size, say.) The implementation may be simple enough to make this a negligible consideration, though.I just would like to express that I think a bare try(foo())
actually bailing out of the calling function takes away from us the visual cue that function flow may change depending on the result.
I feel I can work with try
given enough getting used, but I also do feel we will need extra IDE support (or some such) to highlight try
to efficiently recognize the implicit flow in code reviews/debugging sessions
The thing I'm most concerned about is the need to have named return values just so that the defer statement is happy.
I think the overall error handling issue that the community complains about is a combination of the boilerplate of if err != nil
AND adding context to errors. The FAQ clearly states that the latter is left out intentionally as a separate problem, but I feel like then this becomes an incomplete solution, but I'll be willing to give it a chance after thinking on these 2 things:
err
at the beginning of the function.func sample() (string, error) {
var err error
defer fmt.HandleErrorf(&err, "whatever")
s := try(f())
return s, nil
}
wrapf
function that has the if err != nil
boilerplate.func sample() (string, error) {
s, err := f()
try(wrapf(err, "whatever"))
return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
if err != nil {
// err = wrapped error
}
return err
}
If either work, I can deal with it.
func sample() (string, error) {
var err error
defer fmt.HandleErrorf(&err, "whatever")
s := try(f())
return s, nil
}
This will not work. The defer will update the local err
variable, which is unrelated to the return value.
func sample() (string, error) {
s, err := f()
try(wrapf(err, "whatever"))
return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
if err != nil {
// err = wrapped error
}
return err
}
That should work. It will call wrapf even on a nil error, though.
This will also (continue to) work, and is IMO a lot clearer:
func sample() (string, error) {
s, err := f()
if err != nil {
return "", wrap(err)
}
return s, nil
}
No one is going to make you use try
.
Not sure why anyone ever would write a function like this but what would be the envisioned output for
try(foobar())
If
foobar
returned(error, error)
Why would you return more than one error from a function? If you are returning more than one error from function, perhaps function should be split into two separate ones in the first place, each returning just one error.
Could you elaborate with an example?
@cespare: It should be possible for somebody to write a go fix
that rewrites existing code suitable for try
such that it uses try
. It may be useful to get a feel for how existing code could be simplified. We don't expect any significant changes in code size or performance, since try
is just syntactic sugar, replacing a common pattern by a shorter piece of source code that produces essentially the same output code. Note also that code that uses try
will be bound to use a Go version that's at least the version at which try
was introduced.
@lestrrat: Agreed that one will have to learn that try
can change control flow. We suspect that IDE's could highlight that easily enough.
@Goodwine: As @randall77 already pointed out, your first suggestion won't work. One option we have thought about (but not discussed in the doc) is the possibility of having some predeclared variable that denotes the error
result (if one is present in the first place). That would eliminate the need for naming that result just so it can be used in a defer
. But that would be even more magic; it doesn't seem justified. The problem with naming the return result is essentially cosmetic, and where that matters most is in the auto-generated APIs served by go doc
and friends. It would be easy to address this in those tools (see also the detailed design doc's FAQ on this subject).
@nictuku: Regarding your suggestion for clarification (s/before/and/): I think the code immediately before the paragraph you're referring to makes it clear what happens exactly, but I see your point, s/before/and/ may make the prose clearer. I'll make the change.
See CL 180637.
I actually really like this proposal. However, I do have one criticism. The exit point of functions in Go have always been marked by a return
. Panics are also exit points, however those are catastrophic errors that are typically not meant to ever be encountered.
Making an exit point of a function that isn't a return
, and is meant to be commonplace, may lead to much less readable code. I had heard about this in a talk and it is hard to unsee the beauty of how this code is structured:
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if err := w.Close(); err != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}
This code may look like a big mess, and was _meant_ to by the error handling draft, but let's compare it to the same thing with try
.
func CopyFile(src, dst string) error {
defer func() {
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}()
r, err := try(os.Open(src))
defer r.Close()
w, err := try(os.Create(dst))
defer w.Close()
defer os.Remove(dst)
try(io.Copy(w, r))
try(w.Close())
return nil
}
You may look at this at first glance and think it looks better, because there is a lot less repeated code. However, it was very easy to spot all of the spots that the function returned in the first example. They were all indented and started with return
, followed by a space. This is because of the fact that all conditional returns _must_ be inside of conditional blocks, thereby being indented by gofmt
standards. return
is also, as previously stated, the only way to leave a function without saying that a catastrophic error occurred. In the second example, there is only a single return
, so it looks like the only thing that the function _ever_ should return is nil
. The last two try
calls are easy to see, but the first two are a bit harder, and would be even harder if they were nested somewhere, ie something like proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1]))))
.
Returning from a function has seemed to have been a "sacred" thing to do, which is why I personally think that all exit points of a function should be marked by return
.
Someone has already implemented this 5 years ago. If you are interested, you can
try this feature
https://news.ycombinator.com/item?id=20101417
I implemented try() in Go five years ago with an AST preprocessor and used it in real projects, it was pretty nice: https://github.com/lunixbochs/og
Here are some examples of me using it in error-check-heavy functions: https://github.com/lunixbochs/poxd/blob/master/tls.go#L13
I appreciate the effort that went into this. I think it's the most go-ey solution I've seen so far. But I think it introduces a bunch of work when debugging. Unwrapping try and adding an if block every time I debug and rewrapping it when I'm done is tedious. And I also have some cringe about the magical err variable that I need to consider. I've never been bothered by the explicit error checking so perhaps I'm the wrong person to ask. It always struck me as "ready to debug".
@griesemer
My problem with your proposed use of defer as a way to handle the error wrapping is that the behavior from the snippet I showed (repeated below) is not very common AFAICT, and because it's very rare then I can imagine people writing this thinking it works when it doesn't.
Like.. a beginner wouldn't know this, if they have a bug because of this they won't go "of course, I need a named return", they would get stressed out because it should work and it doesn't.
var err error
defer fmt.HandleErrorf(err);
try
is already too magic so you may as well go all the way and add that implicit error value. Think on the beginners, not on those who know all the nuances of Go. If it's not clear enough, I don't think it's the right solution.
Or... Don't suggest using defer like this, try another way that's safer but still readable.
@deanveloper It is true that this proposal (and for that matter, any proposal trying to attempt the same thing) will remove explicitly visible return
statements from the source code - that is the whole point of the proposal after all, isn't it? To remove the boilerplate of if
statements and returns
that are all the same. If you want to keep the return
's, don't use try
.
We are used to immediately recognize return
statements (and panic
's) because that's how this kind of control flow is expressed in Go (and many other languages). It seems not far fetched that we will also recognize try
as changing control flow after some getting used to it, just like we do for return
. I have no doubt that good IDE support will help with this as well.
I have two concerns:
In my experience, adding context to errors immediately after each call site is critical to having code that can be easily debugged. And named returns have caused confusion for nearly every Go developer I know at some point.
A more minor, stylistic concern is that it's unfortunate how many lines of code will now be wrapped in try(actualThing())
. I can imagine seeing most lines in a codebase wrapped in try()
. That feels unfortunate.
I think these concerns would be addressed with a tweak:
a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)
check()
would behave much like try()
, but would drop the behavior of passing through function return values generically, and instead would provide the ability to add context. It would still trigger a return.
This would retain many of the advantages of try()
:
errors.Wrap(err, "context message")
a, b, err := myFunc()
linedefer fmt.HandleError(&err, "msg")
is still possible, but doesn't need to be encouraged.check
is slightly simpler, because it doesn't need to return an arbitrary number of arguments from the function it is wrapping.@s4n-gt Thanks for this link. I was not aware of it.
@Goodwine Point taken. The reason for not providing more direct error handling support is discussed in the design doc in detail. It is also a fact that over the course of a year or so (since the draft designs published at last year's Gophercon) no satisfying solution for explicit error handling has come up. Which is why this proposal leaves this out on purpose (and instead suggests to use a defer
). This proposal still leaves the door open for future improvements in that regard.
The proposal mentions changing package testing to allow tests and benchmarks to return an error. Though it wouldn’t be “a modest library change”, we could consider accepting func main() error
as well. It’d make writing little scripts much nicer. The semantics would be equivalent to:
func main() {
if err := newmain(); err != nil {
println(err.Error())
os.Exit(1)
}
}
One last criticism. Not really a criticism to the proposal itself, but instead a criticism to a common response to the "function controlling flow" counterargument.
The response to "I don't like that a function is controlling flow" is that "panic
also controls the flow of the program!". However, there are a few reasons that it's more okay for panic
to do this that don't apply to try
.
panic
is friendly to beginner programmers because what it does is intuitive, it continues unwrapping the stack. One shouldn't even have to look up how panic
works in order to understand what it does. Beginner programmers don't even need to worry about recover
, since beginners aren't typically building panic recovery mechanisms, especially since they are nearly always less favorable than simply avoiding the panic in the first place.
panic
is a name that is easy to see. It brings worry, and it needs to. If one sees panic
in a codebase, they should be immediately thinking of how to _avoid_ the panic, even if it's trivial.
Piggybacking off of the last point, panic
cannot be nested in a call, making it even easier to see.
It is okay for panic to control the flow of the program because it is extremely easy to spot, and it is intuitive as to what it does.
The try
function satisfies none of these points.
One cannot guess what try
does without looking up the documentation for it. Many languages use the keyword in different ways, making it hard to understand what it would mean in Go.
try
does not catch my eye, especially when it is a function. _Especially_ when syntax highlighting will highlight it as a function. _ESPECIALLY_ after developing in a language like Java, where try
is seen as unnecessary boilerplate (because of checked exceptions).
try
can be used in an argument to a function call, as per my example in my previous comment proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1]))))
. This makes it even harder to spot.
My eyes ignore the try
functions, even when I am specifically looking for them. My eyes will see them, but immediately skip to the os.FindProcess
or strconv.Atoi
calls. try
is a conditional return. Control flow AND returns are both held up on pedestals in Go. All control flow within a function is indented, and all returns begin with return
. Mixing both of these concepts together into an easy-to-miss function call just feels a bit off.
This comment and my last are my only real criticisms to the idea though. I think I may be coming off as not liking this proposal, but I still think that it is an overall win for Go. This solution still feels more Go-like than the other solutions. If this were added I would be happy, however I think that it can still be improved, I'm just not sure how.
@buchanae interesting. As written, though, it moves fmt-style formatting from a package into the language itself, which opens up a can of worms.
As written, though, it moves fmt-style formatting from a package into the language itself, which opens up a can of worms.
Good point. A simpler example:
a, b, err := myFunc()
check(err, "calling myFunc")
@buchanae We have considered making explicit error handling more directly connected with try
- please see the detailed design doc, specifically the section on Design iterations. Your specific suggestion of check
would only allow to augment errors through something like a fmt.Errorf
like API (as part of the check
), if I understand correctly. In general, people may want to do all kinds of things with errors, not just create a new one that refers to the original one via its error string.
Again, this proposal does not attempt to solve all error handling situations. I suspect in most cases try
makes sense for code that now looks basically like this:
a, b, c, ... err := try(someFunctionCall())
if err != nil {
return ..., err
}
There is an awful lot of code that looks like this. And not every piece of code looking like this needs more error handling. And where defer
is not right, one can still use an if
statement.
I don’t follow this line:
defer fmt.HandleErrorf(&err, “foobar”)
It drops the inbound error on the floor, which is unusual. Is it meant to be used something more like this?
defer fmt.HandleErrorf(&err, “foobar: %v”, err)
The duplication of err is a bit stutter-y. This is not really directly apropos to the proposal, just a side comment about the doc.
I share the two concerns raised by @buchanae, re: named returns and contextual errors.
I find named returns a bit troublesome as it is; I think they are only really beneficial as documentation. Leaning on them more heavily is a worry. Sorry to be so vague, though. I'll think about this more and provide some more concrete thoughts.
I do think there is a real concern that people will strive to structure their code so that try
can be used, and therefore avoid adding context to errors. This is a particularly weird time to introduce this, given we're just now providing better ways to add context to errors through official error wrapping features.
I do think that try
as-proposed makes some code significantly nicer. Here's a function I chose more or less at random from my current project's code base, with some of the names changed. I am particularly impressed by how try
works when assigning to struct fields. (That is assuming my reading of the proposal is correct, and that this works?)
The existing code:
func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
err := dbfile.RunMigrations(db, dbMigrations)
if err != nil {
return nil, err
}
t := &Thing{
thingy: thingy,
}
t.scanner, err = newScanner(thingy, db, client)
if err != nil {
return nil, err
}
t.initOtherThing()
return t, nil
}
With try
:
func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
try(dbfile.RunMigrations(db, dbMigrations))
t := &Thing{
thingy: thingy,
scanner: try(newScanner(thingy, db, client)),
}
t.initOtherThing()
return t, nil
}
No loss of readability, except perhaps that it's less obvious that newScanner
might fail. But then in a world with try
Go programmers would be more sensitive to its presence.
@josharian Regarding main
returning an error
: It seems to me that your little helper function is all that's needed to get the same effect. I'm not sure changing the signature of main
is justified.
Regarding the "foobar" example: It's just a bad example. I should probably change it. Thanks for bringing it up.
defer fmt.HandleErrorf(&err, “foobar: %v”, err)
Actually, that can’t be right, because err
will be evaluated too early. There are a couple of ways around this, but none of them as clean as the original (I think flawed) HandleErrorf. I think it’d be good to have a more realistic worked example or two of a helper function.
EDIT: this early evaluation bug is present in an example
near the end of the doc:
defer fmt.HandleErrorf(&err, "copy %s %s: %v", src, dst, err)
@adg Yes, try
can be used as you're using it in your example. I let your comments re: named returns stand as is.
people may want to do all kinds of things with errors, not just create a new one that refers to the original one via its error string.
try
doesn't attempt to handle all the kinds of things people want to do with errors, only the ones that we can find a practical way to make significantly simpler. I believe my check
example walks the same line.
In my experience, the most common form of error handling code is code that essentially adds a stack trace, sometimes with added context. I've found that stack trace to be very important for debugging, where I follow an error message through the code.
But, maybe other proposals will add stack traces to all errors? I've lost track.
In the example @adg gave, there are two potential failures but no context. If newScanner
and RunMigrations
don't themselves provide messages that clue you into which one went wrong, then you're left guessing.
In the example @adg gave, there are two potential failures but no context. If newScanner and RunMigrations don't themselves provide messages that clue you into which one went wrong, then you're left guessing.
That's right, and that's the design choice we made in this particular piece of code. We do wrap errors a lot in other parts of the code.
I share the concern as @deanveloper and others that it might make debugging tougher. It's true that we can choose not to use it, but the styles of third-party dependencies are not under our control.
If less repetitive if err := ... { return err }
is the primary point, I wonder if a "conditional return" would suffice, like https://github.com/golang/go/issues/27794 proposed.
return nil, err if f, err := os.Open(...)
return nil, err if _, err := os.Write(...)
I think the ?
would be a better fit than try
, and always having to chase the defer
for error would also be tricky.
This also closes the gates for having exceptions using try/catch
forever.
This also closes the gates for having exceptions using try/catch forever.
I am _more_ than okay with this.
I agree with some of the concerns raised above regarding adding context to an error. I am slowly trying to shift from just returning an error to always decorate it with a context and then returning it. With this proposal, I will have to completely change my function to use named return params (which I feel is odd because I barely use naked returns).
As @griesemer says:
Again, this proposal does not attempt to solve all error handling situations. I suspect in most cases try makes sense for code that now looks basically like this:
a, b, c, ... err := try(someFunctionCall())
if err != nil {
return ..., err
}
There is an awful lot of code that looks like this. And not every piece of code looking like this needs more error handling. And where defer is not right, one can still use an if statement.
Yes, but shouldn't good, idiomatic code always wrap/decorate their errors ? I believe that's why we are introducing refined error handling mechanisms to add context/wrap errors in stdlib. As I see, this proposal only seems to consider the most basic use case.
Moreover, this proposal addresses only the case of wrapping/decorating multiple possible error return sites at a _single place_, using named parameters with a defer call.
But it doesn't do anything for the case when one needs to add different contexts to different errors in a single function. For eg, it is very essential to decorate the DB errors to get more information on where they are coming from (assuming no stack traces)
This is an example of a real code I have -
func (p *pgStore) DoWork() error {
tx, err := p.handle.Begin()
if err != nil {
return err
}
var res int64
err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
if err != nil {
tx.Rollback()
return fmt.Errorf("insert table: %w", err)
}
_, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
if err != nil {
tx.Rollback()
return fmt.Errorf("insert table2: %w", err)
}
return tx.Commit()
}
According to the proposal:
If error augmentation or wrapping is desired there are two approaches: Stick with the tried-and-true if statement, or, alternatively, “declare” an error handler with a defer statement:
I think this will fall into the category of "stick with the tried-and-true if statement". I hope the proposal can be improved to address this too.
I strongly suggest the Go team prioritize generics, as that's where Go hears the most criticism, and wait on error-handling. Today's technique is not that painful (tho go fmt
should let it sit on one line).
The try()
concept has all the problems of check
from check/handle:
It doesn't read like Go. People want assignment syntax, without the subsequent nil test, as that looks like Go. Thirteen separate responses to check/handle suggested this; see _Recurring Themes_ here:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes
f, # := os.Open(...) // return on error
f, #panic := os.Open(...) // panic on error
f, #hname := os.Open(...) // invoke named handler on error
// # is any available symbol or unambiguous pair
Nesting of function calls that return errors obscures the order of operations, and hinders debugging. The state of affairs when an error occurs, and therefore the call sequence, should be clear, but here it’s not:
try(step4(try(step1()), try(step3(try(step2())))))
Now recall that the language forbids:
f(t ? a : b)
and f(a++)
It would be trivial to return errors without context. A key rationale of check/handle was to encourage contextualization.
It's tied to type error
and the last return value. If we need to inspect other return values/types for exceptional state, we're back to: if errno := f(); errno != 0 { ... }
It doesn't offer multiple pathways. Code that calls storage or networking APIs handles such errors differently than those due to incorrect input or unexpected internal state. My code does one of these far more often than return err
:
@gopherbot add Go2, LanguageChange
How about use only ?
to unwrap result just like rust
The reason we are skeptical about calling try() may be two implicit binding. We can not see the binding for the return value error and arguments for try(). For about try(), we can make a rule that we must use try() with argument function which have error in return values. But binding to return values are not. So I'm thinking more expression is required for users to understand what this code doing.
func doSomething() (int, %error) {
f := try(foo())
...
}
%error
in return values.It is hard to add new requirements/feature to the existing syntax.
To be honest, I think that foo() should also have %error.
Add 1 more rule
In the detailed design document I noticed that in an earlier iteration it was suggested to pass an error handler to the try builtin function. Like this:
handler := func(err error) error {
return fmt.Errorf("foo failed: %v", err) // wrap error
}
f := try(os.Open(filename), handler)
or even better, like this:
f := try(os.Open(filename), func(err error) error {
return fmt.Errorf("foo failed: %v", err) // wrap error
})
Although, as the document states, that this raises several questions, I think this proposal would be far more more desirable and useful if it had kept this possibility to optionally specify such an error handler function or closure.
Secondly, I don't mind that a built in that can cause the function to return, but, to bikeshed a bit, the name 'try' is too short to suggest that it can cause a return. So a longer name, like attempt
seems better to me.
EDIT: Thirdly, ideally, go language should gain generics first, where an important use case would be the ability to implement this try function as a generic, so the bikeshedding can end, and everyone can get the error handling that they prefer themselves.
Hacker news has some point: try
doesn't behave like a normal function (it can return) so it's not good to give it function-like syntax. A return
or defer
syntax would be more appropriate:
func CopyFile(src, dst string) (err error) {
r := try os.Open(src)
defer r.Close()
w := try os.Create(dst)
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
try io.Copy(w, r)
try w.Close()
return nil
}
@sheerun the common counterargument to this is that panic
is also a control-flow altering built-in function. I personally disagree with it, however it is correct.
panic(...)
is a relatively clear exception (pun not intended) to the rule that return
is the only way out of a function. I don't think we should use its existence as justification to add a third.maybe we can add a variant with optional augmenting function something like tryf
with this semantics:
func tryf(t1 T1, t1 T2, … tn Tn, te error, fn func(error) error) (T1, T2, … Tn)
translates this
x1, x2, … xn = tryf(f(), func(err error) { return fmt.Errorf("foobar: %q", err) })
into this
t1, … tn, te := f()
if te != nil {
if fn != nil {
te = fn(te)
}
err = te
return
}
since this is an explicit choice (instead of using try
) we can find reasonable answers the questions in the earlier version of this design. for example if augmenting function is nil don't do anything and just return the original error.
I'm concerned that try
will supplant traditional error handling, and that that will make annotating error paths more difficult as a result.
Code that handles errors by logging messages and updating telemetry counters will be looked upon as defective or improper by both linters and developers expecting to try
everything.
a, b, err := doWork()
if err != nil {
updateCounters()
writeLogs()
return err
}
Go is an extremely social language with common idioms enforced by tooling (fmt, lint, etc). Please keep the social ramifications of this idea in mind - there will be a tendency to want to use it everywhere.
@politician, sorry, but the word you are looking for is not _social_ but _opinionated_. Go is an opinionated programming language. For the rest I mostly agree with what you are getting at.
@beoran Community tools like Godep and the various linters demonstrate that Go is both opinionated and social, and many of the dramas with the language stem from that combination. Hopefully, we can both agree that try
shouldn't be the next drama.
@politician Thanks for clarifying, I hadn't understood it that way. I can certainly agree that we should try to avoid drama.
I am confused about it.
From the blog: Errors are values, from my perspective, it's designed to be valued not to be ignored.
And I do believe what Rop Pike said, "Values can be programmed, and since errors are values, errors can be programmed.".
We should not consider error
as exception
, it's like importing complexity not only for thinking but also for coding if we do so.
"Use the language to simplify your error handling." -- Rob Pike
And more, we can review this slide
One situation where I find error checking via if
particularly awkward is when closing files (e.g. on NFS). I guess, currently we are meant to write the following, if error returns from .Close()
are possible?
r, err := os.Open(src)
if err != nil {
return err
}
defer func() {
// maybe check whether a previous error occured?
return r.Close()
}()
Could defer try(r.Close())
be a good way to have a manageable syntax for some way of dealing with such errors? At least, it would make sense to adjust the CopyFile()
example in the proposal in some way, to not ignore errors from r.Close()
and w.Close()
.
@seehuhn Your example won't compile because the deferred function does not have a return type.
func doWork() (err error) {
r, err := os.Open(src)
if err != nil {
return err
}
defer func() {
err = r.Close() // overwrite the return value
}()
}
Will work like you expect. The key is the named return value.
I like the proposal but I think that the example of @seehuhn should be adressed as well :
defer try(w.Close())
would return the error from Close() only if the error was not already set.
This pattern is used so often...
I agree with the concerns regarding adding context to errors. I see it as one of the best practices that keeps error messages much friendly (and clear) and makes debug process easier.
The first thing I thought about was to replace the fmt.HandleErrorf
with a tryf
function, that prefixs the error with additional context.
func tryf(t1 T1, t1 T2, … tn Tn, te error, ts string) (T1, T2, … Tn)
For example (from a real code I have):
func (c *Config) Build() error {
pkgPath, err := c.load()
if err != nil {
return nil, errors.WithMessage(err, "load config dir")
}
b := bytes.NewBuffer(nil)
if err = templates.ExecuteTemplate(b, "main", c); err != nil {
return nil, errors.WithMessage(err, "execute main template")
}
buf, err := format.Source(b.Bytes())
if err != nil {
return nil, errors.WithMessage(err, "format main template")
}
target := fmt.Sprintf("%s.go", filename(pkgPath))
if err := ioutil.WriteFile(target, buf, 0644); err != nil {
return nil, errors.WithMessagef(err, "write file %s", target)
}
// ...
}
Can be changed to something like:
func (c *Config) Build() error {
pkgPath := tryf(c.load(), "load config dir")
b := bytes.NewBuffer(nil)
tryf(emplates.ExecuteTemplate(b, "main", c), "execute main template")
buf := tryf(format.Source(b.Bytes()), "format main template")
target := fmt.Sprintf("%s.go", filename(pkgPath))
tryf(ioutil.WriteFile(target, buf, 0644), fmt.Sprintf("write file %s", target))
// ...
}
Or, if I take @agnivade's example:
func (p *pgStore) DoWork() (err error) {
tx := tryf(p.handle.Begin(), "begin transaction")
defer func() {
if err != nil {
tx.Rollback()
}
}()
var res int64
tryf(tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res), "insert table")
_, = tryf(tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res), "insert table2")
return tryf(tx.Commit(), "commit transaction")
}
However, @josharian raised a good point that makes me hesitate on this solution:
As written, though, it moves fmt-style formatting from a package into the language itself, which opens up a can of worms.
I'm totally on board with this proposal and can see its benefits across a number of examples.
My only concern with the proposal is the naming of try
, I feel that its connotations with other languages, may skew deveopers perceptions of what its purpose is when coming from other languages. Java comes to find here.
For me, i would prefer the builtin to be called pass
. I feel this gives a better representation of what is happening. After-all you are not handling the error - rather passing it back to be handled by the caller. try
gives the impression that the error has been handled.
It's a thumbs down from me, principally because the problem it's aiming to address ("the boilerplate if statements typically associated with error handling") simply isn't a problem for me. If all error checks were simply if err != nil { return err }
then I could see some value in adding syntactic sugar for that (though Go is a relatively sugar-free language by inclination).
In fact, what I want to do in the event of a non-nil error varies quite considerably from one situation to the next. Maybe I want to t.Fatal(err)
. Maybe I want to add a decorating message return fmt.Sprintf("oh no: %v", err)
. Maybe I just log the error and continue. Maybe I set an error flag on my SafeWriter object and continue, checking the flag at the end of some sequence of operations. Maybe I need to take some other actions. None of these can be automated with try
. So if the argument for try
is that it will eliminate all if err != nil
blocks, that argument doesn't stand.
Will it eliminate _some_ of them? Sure. Is that an attractive proposition for me? Meh. I'm genuinely not concerned. To me, if err != nil
is just part of Go, like the curly braces, or defer
. I understand it looks verbose and repetitive to people who are new to Go, but people who are new to Go are not best placed to make dramatic changes to the language, for a whole bunch of reasons.
The bar for significant changes to Go has traditionally been that the proposed change must solve a problem that's (A) significant, (B) affects a lot of people, and (C) is well solved by the proposal. I'm not convinced on any of these three criteria. I'm quite happy with Go's error handling as it is.
To echo @peterbourgon and @deanveloper, one of my favourite things about Go is that code flow is clear and panic() is not treated like a standard flow control mechanism in the way it is in Python.
Regarding the debate on panic, panic() almost always appears by itself on a line because it has no value. You can't fmt.Println(panic("oops"))
. This increases its visibility tremendously and makes it far less comparable to try()
than people are making out.
If there is to be another flow control construct for functions, I would _far_ prefer that it be a statement guaranteed to be the leftmost item on a line.
One of the examples in the proposal nails the problem for me:
func printSum(a, b string) error {
fmt.Println(
"result:",
try(strconv.Atoi(a)) + try(strconv.Atoi(b)),
)
return nil
}
Control flow really becomes less obvious and very obscured.
This is also against the initial intention by Rob Pike that all errors need to be handled explicitly.
While a reaction to this can be "then don't use it", the problem is -- other libraries will use it, and debugging them, reading them, and using them, becomes more problematic. This will motivate my company to never adopt go 2, and start using only libraries that don't use try
. If I'm not alone with this, it might lead to a division a-la python 2/3.
Also, the naming of try
will automatically imply that eventually catch
will show up in the syntax, and we'll be back to being Java.
So, because of all of this, I'm _strongly_ against this proposal.
I don't like the try
name. It implies an _attempt_ at doing something with a high risk of failure (I may have a cultural bias against _try_ as I'm not a native english speaker), while instead try
would be used in case we expect rare failures (motivation for wanting to reduce verbosity of error handling) and are optimistic. In addition try
in this proposal does in fact _catches_ an error to return it early. I like the pass
suggestion of @HiImJC.
Besides the name, I find awkward to have return
-like statement now hidden in the middle of expressions. This breaks Go flow style. It will make code reviews harder.
In general, I find that this proposal will only benefit to the lazy programmer who has now a weapon for shorter code and even less reason to make the effort of wrapping errors. As it will also make reviews harder (return in middle of expression), I think that this proposal goes against the "programming at scale" aim of Go.
One of my favourite things about Go that I generally say when describing the language is that there is only one way to do things, for most things. This proposal goes against that principle a bit by offering multiple ways to do the same thing. I personally think this is not necessary and that it would take away, rather than add to the simplicity and readability of the language.
I like this proposal overall. The interaction with defer
seems sufficient to provide an ergonomic way of returning an error while also adding additional context. Though it would be nice to address the snag @josharian pointed out around how to include the original error in the wrapped error message.
What's missing is an ergonomic way of this interacting with the error inspection proposal(s) on the table. I believe API's should be very deliberate in what types of errors they return, and the default should probably be "returned errors are not inspectable in any way". It should then be easy to go to a state where errors are inspectable in a precise way, as documented by the function signature ("It reports an error of kind X in circumstance A and an error of kind Y in circumstance B").
Unfortunately, as of now, this proposal makes the most ergonomic option the most undesirable (to me); blindly passing through arbitrary error kinds. I think this is undesirable because it encourages not thinking about the kinds of errors you return and how users of your API will consume them. The added convenience of this proposal is certainly nice, but I fear it will encourage bad behavior because the perceived convenience will outweigh the perceived value of thinking carefully about what error information you provide (or leak).
A bandaid would be if errors returned by try
get converted into errors that are not "unwrappable". Unfortunately this has pretty severe downsides as well, since it makes it so that any defer
could not inspect the errors itself. Additionally it prevents the usage where try
actually will return an error of a desirable kind (that is, use cases where try
is used carefully rather than carelessly).
Another solution would be to repurpose the (discarded) idea of having an optional second argument to try
for defining/whitelisting the error kind(s) that may be returned from that site. This is a bit troublesome because we have two different ways of defining an "error kind", either by value (io.EOF
etc) or by type (*os.PathError
, *exec.ExitError
). It's easy to specify error kinds that are values as arguments to a function, but harder to specify types. Not sure how to handle that, but throwing the idea out there.
The problem that @josharian pointed out can be avoided by delaying the evaluation of err:
defer func() { fmt.HandleErrorf(&err, "oops: %v", err) }()
Doesn't look great, but it should work. I'd prefer however if this can be addressed by adding a new formatting verb/flag for error pointers, or maybe for pointers in general, that prints the dereferenced value as with plain %v
. For the purpose of the example, let's call it %*v
:
defer fmt.HandleErrorf(&err, "oops: %*v", &err)
The snag aside, I think that this proposal looks promising, but it seems crucial to keep the ergonomics of adding context to errors in check.
Edit:
Another approach is to wrap the error pointer in a struct that implements Stringer
:
type wraperr struct{ err *error }
func (w wraperr) String() string { return (*w.err).Error() }
...
defer handleErrorf(&err, "oops: %v", wraperr{&err})
Couple of things from my perspective. Why are we so concerned about saving a few lines of code? I consider this along the same lines as Small functions considered harmful.
Additionally I find that such a proposal would remove the responsibility of correctly handling the error to some "magic" that I worry will just be abused and encourage laziness resulting in poor quality code and bugs.
The proposal as stated also has a number of unclear behaviors so this is already problematic than an _explicit_ extra ~3 lines that are more clear.
We currently use the defer pattern sparingly in house. There's an article here which had similarly mixed reception when we wrote it - https://bet365techblog.com/better-error-handling-in-go
However, our usage of it was in anticipation of the check
/handle
proposal progressing.
Check/handle was a much more comprehensive approach to making error handling in go more concise. Its handle
block retained the same function scope as the one it was defined in, whereas any defer
statements are new contexts with an amount, however much, of overhead. This seemed to be more in keeping with go's idioms, in that if you wanted the behaviour of "just return the error when it happens" you could declare that explicitly as handle { return err }
.
Defer obviously relies on the err reference being maintained also, but we've seen problems arise from shadowing the error reference with block scoped vars. So it isn't fool proof enough to be considered the standard way of handling errors in go.
try
, in this instance, doesn't appear to solve too much and I share the same fear as others that it would simply lead to lazy implementations, or ones which over-use the defer pattern.
If defer-based error handling is going to be A Thing, then something like this should probably be added to the errors package:
f := try(os.Create(filename))
defer errors.Deferred(&err, f.Close)
Ignoring the errors of deferred Close statements is a pretty common issue. There should be a standard tool to help with it.
A builtin function that returns is a harder sell than a keyword that does the same.
I would like it more if it were a keyword like it is in Zig[1].
Built-in functions, whose type signature cannot be expressed using the language's type system, and whose behavior confounds what a function normally is, just seems like an escape hatch that can be used repeatedly to avoid actual language evolution.
We are used to immediately recognize return statements (and panic's) because that's how this kind of control flow is expressed in Go (and many other languages). It seems not far fetched that we will also recognize try as changing control flow after some getting used to it, just like we do for return. I have no doubt that good IDE support will help with this as well.
I think it is fairly far-fetched. In gofmt'ed code, a return always matches /^\t*return /
– it's a very trivial pattern to spot by eye, without any assistance. try
, on the other hand, can occur anywhere in the code, nested arbitrarily deep in function calls. No amount of training will make us be able to immediately spot all control flow in a function without tool assistance.
Furthermore, a feature that depends on "good IDE support" will be at a disadvantage in all the environments where there is no good IDE support. Code review tools come to mind immediately – will Gerrit highlight all the try's for me? What about people who choose not to use IDEs, or fancy code highlighting, for various reasons? Will acme start highlighting try
?
A language feature should be easy to understand on its own, not depend on editor support.
@kungfusheep I like that article. Taking care of wrapping in a defer alone already drives up readability quite a bit without try
.
I'm in the camp that doesn't feel errors in Go are really a problem. Even so, if err != nil { return err }
can be quite the stutter on some functions. I've written functions that needed an error check after almost every statement and none needed any special handling other than wrap and return. Sometimes there just isn't any clever Buffer struct that's gonna make things nicer. Sometimes it's just a different critical step after another and you need to simply short circuit if something went wrong.
Although try
would certainly make that code a lot easier to nicer to read while being fully backwards compatible, I agree that try
isn't a critical must have feature, so if people are too scared of it maybe it's best not to have it.
The semantics are quite clear cut though. Anytime you see try
it's either following the happy path, or it returns. I really can't get simpler than that.
This looks like a special cased macro.
@dominikh try
always matches /try\(/
so I don't know what your point is really. It's equally as searchable and every editor I've ever heard of has a search feature.
@qrpnxz I think the point he was trying to make is not that you cannot search for it programatically, but that it's harder to search for with your eyes. The regexp was just an analogy, with emphasis on the /^\t*
, signifying that all returns clearly stand out by being at the beginning of a line (ignoring leading whitespace).
Thinking about it more, there should be a couple of common helper functions. Perhaps they should be in a package called "deferred".
Addressing the proposal for a check
with format to avoid naming the return, you can just do that with a function that checks for nil, like so
func Format(err error, message string, args ...interface{}) error {
if err == nil {
return nil
}
return fmt.Errorf(...)
}
This can be used without a named return like so:
func foo(s string) (int, error) {
n, err := strconv.Atoi(s)
try(deferred.Format(err, "bad string %q", s))
return n, nil
}
The proposed fmt.HandleError could be put into the deferred package instead and my errors.Defer helper func could be called deferred.Exec
and there could be a conditional exec for procedures to execute only if the error is non-nil.
Putting it together, you get something like
func CopyFile(src, dst string) (err error) {
defer deferred.Annotate(&err, "copy %s %s", src, dst)
r := try(os.Open(src))
defer deferred.Exec(&err, r.Close)
w := try(os.Create(dst))
defer deferred.Exec(&err, r.Close)
defer deferred.Cond(&err, func(){ os.Remove(dst) })
try(io.Copy(w, r))
return nil
}
Another example:
func (p *pgStore) DoWork() (err error) {
tx := try(p.handle.Begin())
defer deferred.Cond(&err, func(){ tx.Rollback() })
var res int64
err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
try(deferred.Format(err, "insert table")
_, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
try(deferred.Format(err, "insert table2"))
return tx.Commit()
}
if err != nil
everywhere, to having try
everywhere. It shifts the proposed problem and does not solve it.Although, I'd argue that the current error handling mechanism is not a problem to begin with. We just need to improve tooling and vetting around it.
Furthermore, I would argue that if err != nil
is actually more readable than try
because it does not clutter the line of the business logic language, rather sits right below it:
file := try(os.OpenFile("thing")) // less readable than,
file, err := os.OpenFile("thing")
if err != nil {
}
And if Go was to be more magical in its error handling, why not just totally own it. For example Go can implicitly call the builtin try
if a user does not assign an error. For example:
func getString() (string, error) { ... }
func caller() {
defer func() {
if err != nil { ... } // whether `err` must be defined or not is not shown in this example.
}
// would call try internally, because a user is not
// assigning an error value. Also, it can add a compile error
// for "defined and not used err value" if the user does not
// handle the error.
str := getString()
}
To me, that would actually accomplish the redundancy problem at the cost of magic and potential readability.
Therefore, I propose that we either truly solve the 'problem' like in the above example or keep the current error handling but instead of changing the language to solve redundancy and wrapping, we don't change the language but we improve the tooling and vetting of code to make the experience better.
For example, in VSCode there's a snippet called iferr
if you type it and hit enter, it expands to a full error handling statement...therefore, writing it never feels tiresome to me, and reading later on is better.
@josharian
Though it wouldn’t be “a modest library change”, we could consider accepting func main() error as well.
The issue with that is that not all platforms have clear semantics on what that means. Your rewrite works well in "traditional" Go programs running on a full operating system - but as soon as you write microcontroller-firmware or even just WebAssembly, it's not super clear what os.Exit(1)
would mean. Currently, os.Exit
is a library-call, so Go implementations are free just not to provide it. The shape of main
is a language concern though.
A question about the proposal that is probably best answered by "nope": How does try
interact with variadic arguments? It's the first case of a variadic (ish) function that doesn't have its variadic-nes in the last argument. Is this allowed:
var e []error
try(e...)
Leaving aside why you'd ever do that. I suspect the answer is "no" (otherwise the follow-up is "what if the length of the expanded slice is 0). Just bringing that up so it can be kept in mind when phrasing the spec eventually.
try
proposal is not consistent with these basic tenets, as it will promote shorthand at the cost of control-flow readability.try
built-in a statement instead of a function. Then it is more consistent with other control-flow statements like if
. Additionally removal of the nested parentheses marginally improves readability.defer
or similar. It already cannot be implemented in pure go (as pointed out by others) so it may as well use a more efficient implementation under the hood.I see two problems with this:
Number 2 I think is far worse. All the examples here are simple calls that return an error, but what's a lot more insidious is this:
func doit(abc string) error {
a := fmt.Sprintf("value of something: %s\n", try(getValue(abc)))
log.Println(a)
return nil
}
This code can exit in the middle of that sprintf, and it's going to be SUPER easy to miss that fact.
My vote is no. This will not make go code better. It won't make it easier to read. It won't make it more robust.
I've said it before, and this proposal exemplifies it - I feel like 90% of the complaints about Go are "I don't want to write an if statement or a loop" . This removes some very simple if statements, but adds cognitive load and makes it easy to miss exit points for a function.
I just want to point out that you could not use this in main and it might be confusing to new users or when teaching. Obviously this applies to any function that doesn't return an error but I think main is special since it appears in many examples..
func main() {
f := try(os.Open("foo.txt"))
defer f.Close()
}
I'm not sure making try panic in main would be acceptable either.
Additionally it would not be particularly useful in tests (func TestFoo(t* testing.T)
) which is unfortunate :(
The issue I have with this is it assumes you always want to just return the error when it happens. When maybe you want to add context to the error and the return it or maybe you just want to behave differently when an error happens. Maybe that is depending on the type of error returned.
I would prefer something akin to a try/catch which might look like
Assuming foo()
defined as
func foo() (int, error) {}
You could then do
n := try(foo()) {
case FirstError:
// do something based on FirstError
case OtherError:
// do something based on OtherError
default:
// default behavior for any other error
}
Which translates to
n, err := foo()
if errors.Is(err, FirstError) {
// do something based on FirstError
if errors.Is(err, OtherError) {
// do something based on OtherError
} else {
// default behavior for any other error
}
To me, error handling is one of the most important parts of a code base.
Already too much go code is if err != nil { return err }
, returning an error from deep in the stack without adding extra context, or even (possibly) worse adding context by masking the underlying error with fmt.Errorf
wrapping.
Providing a new keyword that is kind of magic that does nothing but replace if err != nil { return err }
seems like a dangerous road to go down.
Now all code will just be wrapped in a call to try. This is somewhat fine (though readability sucks) for code that is dealing with only in-package errors such as:
func foo() error {
/// stuff
try(bar())
// more stuff
}
But I'd argue that the given example is really kind of horrific and basically leaves the caller trying to understand an error that is really deep in the stack, much like exception handling.
Of course, this is all up to the developer to do the right thing here, but it gives the developer a great way to not care about their errors with maybe a "we'll fix this later" (and we all know how that goes).
I wish we'd look at the issue from a different perspective than *"how can we reduce repetition" and more about "how can we make (proper) error handling simpler and developers more productive".
We should be thinking about how this will affect running production code.
*Note: This doesn't actually reduce repetition, just changes what's being repeated, all the while making the code less readable because everything is encased in a try()
.
One last point: Reading the proposal at first it seems nice, then you start to get into all the gotchas (at least the ones listed) and it's just like "ok yeah this is too much".
I realize much of this is subjective, but it's something I care about. These semantics are incredibly important.
What I want to see is a way to make writing and maintaining production level code simpler such that you might as well do errors "right" even for POC/demo level code.
Since error context seems to be a recurring theme...
Hypothesis: most Go functions return (T, error)
as opposed to (T1, T2, T3, error)
What if, instead of defining try
as try(T1, T2, T3, error) (T1, T2, T3)
we defined it as
try(func (args) (T1, T2, T3, error))(T1, T2, T3)
? (this is an approximation)
which is to say that the syntactic structure of a try
call is always a first argument that is an expression returning multiple values, the last of which is an error.
Then, much like make
, this opens the door to a 2-argument form of the call, where the second argument is the context of the try (e.g. a fixed string, a string with a %v
, a function that takes an error argument and returns another error etc.)
This still allows chaining for the (T, error)
case but you can no longer chain multiple returns which IMO is typically not required.
@cpuguy83 If you read the proposal you would see there is nothing preventing you from wrapping the error. In fact there are multiple ways of doing it while still using try
. Many people seem to assume that for some reason though.
if err != nil { return err }
is equally as "we'll fix this later" as try
except more annoying when prototyping.
I don't know how things being inside of a pair of parenthesis is less readable than function steps being every four lines of boilerplate either.
It'd be nice if you pointed out some of these particular "gotchas" that bothered you since that's the topic.
Readability seems to be an issue but what about go fmt presenting try() so that it stands out, something like:
f := try(
os.Open("file.txt")
)
@MrTravisB
The issue I have with this is it assumes you always want to just return the error when it happens.
I disagree. It assumes that you want to do so often enough to warrant a shorthand for just that. If you don't, it doesn't get in the way of handling errors plainly.
When maybe you want to add context to the error and the return it or maybe you just want to behave differently when an error happens.
The proposal describes a pattern for adding block-wide context to errors. @josharian pointed out that there is an error in the examples, though, and it's not clear what the best way is to avoid it. I have written a couple of examples of ways to handle it.
For more specific error context, again, try
does a thing, and if you don't want that thing, don't use try
.
@boomlinde Exactly my point. This proposal is trying to solve a singular use case rather than providing a tool to solve the larger issue of error handling. I think the fundamental question if exactly what you pointed out.
It assumes that you want to do so often enough to warrant a shorthand for just that.
In my opinion and experience this use case is a small minority and doesn't warrant shorthand syntax.
Also, the approach of using defer
to handle errors has issues in that it assumes you want to handle all possible errors the same. defer
statements can't be canceled.
defer fmt.HandleErrorf(&err, “foobar”)
n := try(foo())
x : try(foo2())
What if I want different error handling for errors that might be returned from foo()
vs foo2()
?
@MrTravisB
What if I want different error handling for errors that might be returned from foo() vs foo2()?
Then you use something else. That's the point @boomlinde was making.
Maybe you don't personally see this use case often, but many people do, and adding try
doesn't really affect you. In fact, the rarer the use case is to you the less it affects you that try
is added.
@qrpnxz
f := try(os.Open("/foo"))
data := try(ioutil.ReadAll(f))
try(send(data))
(yes I understand there is ReadFile
and that this particular example is not the best way to copy data somewhere, not the point)
This takes more effort to read because you have to parse out the try's inline. The application logic is wrapped up in another call.
I'd also argue that a defer
error handler here would not be good except to just wrap the error with a new message... which is nice but there is more to dealing with errors than making it easy for the human to read what happened.
In rust at least the operator is a postfix (?
added to the end of a call) which doesn't place extra burden to dig out the the actual logic.
panic
may be another flow controlling function, but it doesn't return a value, making it effectively a statement. Compare this to try
, which is an expression and can occur anywhere.
recover
does have a value and affects flow control, but must occur in a defer
statement. These defer
s are typically function literals, recover
is only ever called once, and so recover
also effectively occurs as a statement. Again, compare this to try
which can occur anywhere.
I think those points mean that try
makes it significantly harder to follow control flow in a way that we haven't had before, as has been pointed out before, but I didn't see the distinction between statements and expressions pointed out.
Allow statements like
if err != nil {
return nil, 0, err
}
to be formatted on one line by gofmt
when the block only contains a return
statement and that statement does not contain newlines. For example:
if err != nil { return nil, 0, err }
gofmt
keeps newlines if they already exist (like struct literals). Opt in also allows the writer to make some error handling be emphasizedgofmt
return
statements, so it won't be abused to golf code unnecessarilytry
expressions handles this poorlytry
leans more towards the writertry
existing on multiple lines. For example this comment or this comment which introduces a style likef, err := os.Open(file)
try(maybeWrap(err))
err
value is being returned. Therefore, I suspect this form will be commonly used. Allowing one lined if blocks is almost the same thing, except it's also explicit about what the return values aredefer
based wrapping. Both raise the barrier to wrapping errors and the former may require godoc
changestry
versus using traditional error handlingtry
or something else in the future. The change may be positive even if try
is acceptedtesting
library or main
functions. In fact, if the proposal allows any single lined statement instead of just returns, it may reduce usage of assertion based libraries. Considervalue, err := something()
if err != nil { t.Fatal(err) }
n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }
In summary, this proposal has a small cost, can be designed to be opt-in, doesn't preclude any further changes since it's stylistic only, and reduces the pain of reading verbose error handling code while keeping everything explicit. I think it should at least be considered as a first step before going all in on try
.
Some examples ported
func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
try(dbfile.RunMigrations(db, dbMigrations))
t := &Thing{
thingy: thingy,
scanner: try(newScanner(thingy, db, client)),
}
t.initOtherThing()
return t, nil
}
func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
err := dbfile.RunMigrations(db, dbMigrations))
if err != nil { return nil, fmt.Errorf("running migrations: %v", err) }
t := &Thing{thingy: thingy}
t.scanner, err = newScanner(thingy, db, client)
if err != nil { return nil, fmt.Errorf("creating scanner: %v", err) }
t.initOtherThing()
return t, nil
}
It's competitive in space usage while still allowing for adding context to errors.
func (c *Config) Build() error {
pkgPath := try(c.load())
b := bytes.NewBuffer(nil)
try(emplates.ExecuteTemplate(b, "main", c))
buf := try(format.Source(b.Bytes()))
target := fmt.Sprintf("%s.go", filename(pkgPath))
try(ioutil.WriteFile(target, buf, 0644))
// ...
}
func (c *Config) Build() error {
pkgPath, err := c.load()
if err != nil { return nil, errors.WithMessage(err, "load config dir") }
b := bytes.NewBuffer(nil)
err = templates.ExecuteTemplate(b, "main", c)
if err != nil { return nil, errors.WithMessage(err, "execute main template") }
buf, err := format.Source(b.Bytes())
if err != nil { return nil, errors.WithMessage(err, "format main template") }
target := fmt.Sprintf("%s.go", filename(pkgPath))
err = ioutil.WriteFile(target, buf, 0644)
if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
// ...
}
The original comment used a hypothetical tryf
to attach the formatting, which has been removed. It's unclear the best way to add all the distinct contexts, and perhaps try
wouldn't even be applicable.
@cpuguy83
To me it is more readable with try
. In this example I read "open a file, read all bytes, send data". With regular error handling I would read "open a file, check if there was an error, the error handling does this, then read all bytes, now check if somethings happened..." I know you can scan through the err != nil
s, but to me try
is just easier because when I see it I know the behaviour right away: returns if err != nil. If you have a branch I have to see what it does. It could do anything.
I'd also argue that a defer error handler here would not be good except to just wrap the error with a new message
I'm sure there are other things you can do in the defer, but regardless, try
is for the simple general case anyway. Anytime you want to do something more, there is always good ol' Go error handling. That's not going away.
@zeebo Yep, I'm into that.
@kungfusheep 's article used a one line err check like that and I got exited to try it out. Then as soon as I save, gofmt expanded it into three lines which was sad. Many functions in the stdlib are defined in one line like that so it surprised me that gofmt would expand that out.
@qrpnxz
I happen to read a lot of go code. One of the best things about the language is the ease that comes from most code following a particular style (thanks gofmt).
I don't want to read a bunch of code wrapped in try(f())
.
This means there will either be a divergence in code style/practice, or linters like "oh you should have used try()
here" (which again I don't even like, which is the point of me and others commenting on this proposal).
It is not objectively better than if err != nil { return err }
, just less to type.
One last thing:
If you read the proposal you would see there is nothing preventing you from
Can we please refrain from such language? Of course I read the proposal. It just so happens that I read it last night and then commented this morning after thinking about it and didn't explain the minutia of what I intended.
This is an incredibly adversarial tone.
@cpuguy83
My bad cpu guy. I didn't mean it that way.
And I guess you gotta point that code that uses try
will look pretty different from code that doesn't, so I can imagine that would affect the experience of parsing that code, but I can't totally agree that different means worse in this case, though I understand you personally don't like it just as I personally do like it. Many things in Go are that way. As to what linters tell you to do is another matter entirely, I think.
Sure it's not objectively better. I was expressing that it was more readable that way to me. I carefully worded that.
Again, sorry for sounding that way. Although this is an argument I didn't mean to antagonize you.
https://github.com/golang/go/issues/32437#issuecomment-498908380
No one is going to make you use try.
Ignoring the glibness, I think that's a pretty hand-wavy way to dismiss a design criticism.
Sure, I don't have to use it. But anybody I write code with could use it and force me to try to decipher try(try(try(to()).parse().this)).easily())
. It's like saying
No one is going to make you use the empty interface{}.
Anyway, Go's pretty strict about simplicity: gofmt
makes all the code look the same way. The happy path keeps left and anything that could be expensive or surprising is explicit. try
as is proposed is a 180 degree turn from this. Simplicity != concise.
At the very least try
should be a keyword with lvalues.
It is not _objectively_ better than
if err != nil { return err }
, just less to type.
There is one objective difference between the two: try(Foo())
is an expression. For some, that difference is a downside (the try(strconv.Atoi(x))+try(strconv.Atoi(y))
criticism). For others, that difference is an upside for much the same reason. Still not objectively better or worse - but I also don't think the difference should be swept under the rug and claiming that it's "just less to type" doesn't do the proposal justice.
@elagergren-spideroak hard to say that try
is annoying to see in one breath and then say that it's not explicit in the next. You gotta pick one.
it is common to see function arguments being put into temporary variables first. I'm sure it would be more common to see
this := try(to()).parse().this
that := try(this.easily())
than your example.
try
doing nothing is the happy path, so that looks as expected. In the unhappy path all it does is return. Seeing there is a try
is enough to gather that information. There isn't anything expensive about returning from a function either, so from that description I don't think try
is doing a 180
@josharian Regarding your comment in https://github.com/golang/go/issues/32437#issuecomment-498941854 , I don't think there is an early evaluation error here.
defer fmt.HandleErrorf(&err, “foobar: %v”, err)
The unmodified value of err
is passed to HandleErrorf
, and a pointer to err
is passed. We check whether err
is nil
(using the pointer). If not, we format the string, using the unmodified value of err
. Then we set err
to the formatted error value, using the pointer.
@Merovius The proposal really is just a syntax sugar macro though, so it's gonna end up being about what people think looks nicer or is going to cause the least trouble. If you think not, please explain to me. That's why I'm for it, personally. It's a nice addition without adding any keywords from my perspective.
@ianlancetaylor, I think @josharian is correct: the “unmodified” value of err
is the value at the time the defer
is pushed onto the stack, not the (presumably intended) value of err
set by try
before returning.
The other problem I have with try
is that it makes it so much easier for people to dump more and logic into a single line. This is my major problem with most other languages, is that they make it really easy to put like 5 expressions in a single line, and I don't want that for go.
this := try(to()).parse().this
that := try(this.easily())
^^ even this is downright awful. The first line, I have to jump back and forth doing paren matching in my head. Even the second line which is actually quite simple... is really hard to read.
Nested functions are hard to read. Period.
parser, err := to()
if err != nil {
return err
}
this := parser.parse().this
that, err := this.easily()
if err != nil {
return err
}
^^ This is so much easier and better IMO. It's super simple and clear. yes, it's a lot more lines of code, I don't care. It's very obvious.
@bcmills @josharian Ah, of course, thanks. So it would have to be
defer func() { fmt.HandleErrorf(&err, “foobar: %v”, err) }()
Not so nice. Maybe fmt.HandleErrorf
should implicitly pass the error value as the last argument after all.
This issue has gotten a lot of comments very quickly, and many of them seem to me to be repeating comments that have already been made. Of course feel free to comment, but I would like to gently suggest that if you want to restate a point that has already been made, that you do so by using GitHub's emojis, rather than by repeating the point. Thanks.
@ianlancetaylor if fmt.HandleErrorf
sends err as the first argument after format then the implementation will be nicer and user will be able to reference it by %[1]v
always.
@natefinch Absolutely agree.
I wonder if a rust style approach would be more palatable?
Note this is not a proposal just thinking through it...
this := to()?.parse().this
that := this.easily()?
In the end I think this is nicer, but (could use a !
or something else too...), but still doesn't fix the issue of handling errors well.
of course rust also has try()
pretty much just like this, but... the other rust style.
It is not _objectively_ better than
if err != nil { return err }
, just less to type.There is one objective difference between the two:
try(Foo())
is an expression. For some, that difference is a downside (thetry(strconv.Atoi(x))+try(strconv.Atoi(y))
criticism). For others, that difference is an upside for much the same reason. Still not objectively better or worse - but I also don't think the difference should be swept under the rug and claiming that it's "just less to type" doesn't do the proposal justice.
This is one of the biggest reasons I like this syntax; it lets me use an error-returning function as part of a larger expression without having to name all the intermediate results. In some situations naming them is easy, but in others there's no particularly meaningful or non-redundant name to give them, in which case I'd rather much not give them a name at all.
@MrTravisB
Exactly my point. This proposal is trying to solve a singular use case rather than providing a tool to solve the larger issue of error handling. I think the fundamental question if exactly what you pointed out.
What specifically did I say that is exactly your point? It rather seems to me that you fundamentally misunderstood my point if you think that we agree.
In my opinion and experience this use case is a small minority and doesn't warrant shorthand syntax.
In the Go source there are thousands of cases that could be handled by try
out of the box even if there was no way to add context to errors. If minor, it's still a common cause of complaint.
Also, the approach of using defer to handle errors has issues in that it assumes you want to handle all possible errors the same. defer statements can't be canceled.
Similarly, the approach of using + to handle arithmetic assumes that you don't want to subtract, so you don't if you don't. The interesting question is whether block-wide error context at least represents a common pattern.
What if I want different error handling for errors that might be returned from foo() vs foo2()
Again, then you don't use try
. Then you gain nothing from try
, but you also don't lose anything.
@cpuguy83
I wonder if a rust style approach would be more palatable?
The proposal presents an argument against this.
At this point, I think having try{}catch{}
is more readable :upside_down_face:
defer
corner cases is not only awful for things like godoc, but most importantly it's very error prone. I don't care I can wrap the whole thing with another func()
to go around the issue, it's just more things I need to keep in mind, I think it encourages a "bad practice".No one is going to make you use try.
That doesn't mean it's a good solution, I am making a point that the current idea has a flaw in the design and I'm asking for it to be addressed in a way that is less error prone.
try(try(try(to()).parse().this)).easily())
are unrealistic, this could already be done with other functions and I think it would be fair for those reviewing the code to ask for it to be split.What if I have 3 places that can error-out and I want to wrap each place separately? try()
makes this very hard, in fact try()
is already discouraging wrapping errors given the difficulty of it, but here is an example of what I mean:
func before() error {
x, err := foo()
if err != nil {
wrap(err, "error on foo")
}
y, err := bar(x)
if err != nil {
wrapf(err, "error on bar with x=%v", x)
}
fmt.Println(y)
return nil
}
func after() (err error) {
defer fmt.HandleErrorf(&err, "something failed but I don't know where: %v", err)
x := try(foo())
y := try(bar(x))
fmt.Println(y)
return nil
}
Again, then you don't use
try
. Then you gain nothing fromtry
, but you also don't lose anything.
Let's say it's a good practice to wrap errors with useful context, try()
would be considered a bad practice because it's not adding any context. This means that try()
is a feature nobody wants to use and become a feature that's used so rarely that it may as well not have existed.
Instead of just saying "well, if you don't like it, don't use it and shut up" (this is how it reads), I think it would be better to try to address what many of use are considering a flaw in the design. Can we discuss instead what could be modified from the proposed design so that our concern handled in a better way?
@boomlinde The point that we agree on is that this proposal is trying to solving a minor use case and the fact that "if you don't need, don't use it" is the primary argument for it furthers that point. As @elagergren-spideroak stated, that argument doesn't work because even if I don't want to use it, others will which forces me to use it. By the logic of your argument Go should also have a ternary statement. And if you don't like ternary statements, don't use them.
Disclaimer - I do think Go should have a ternary statement but given that Go's approach to language features is to not introduce features which could make code more difficult to read then it shouldn't.
Another thing occurs to me: I'm seeing a lot of criticism based on the idea that having try
might encourage developers to handle errors carelessly. But in my opinion this is, if anything, more true of the current language; the error-handling boilerplate is annoying enough that it encourages one to swallow or ignore some errors to avoid it. For instance, I've written things like this a few times:
func exists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}
in order to be able to write if exists(...) { ... }
, even though this code silently ignores some possible errors. If I had try
, I probably would not bother to do that and just return (bool, error)
.
Being chaotic here, I'll throw the idea of adding a second built-in function called catch
which will receive a function that takes an error and returns an overwritten error, then if a subsequent catch
is called it would overwrite the handler. for example:
func catch(handler func(err error) error) {
// .. impl ..
}
Now, this builtin function will also be a macro-like function that would handle the next error to be returned by try
like this:
func wrapf(format string, ...values interface{}) func(err error) error {
// user defined
return func(err error) error {
return fmt.Errorf(format + ": %v", ...append(values, err))
}
}
func sample() {
catch(wrapf("something failed in foo"))
try(foo()) // "something failed in foo: <error>"
x := try(foo2()) // "something failed in foo: <error>"
// Subsequent calls for catch overwrite the handler
catch(wrapf("something failed in bar with x=%v", x))
try(bar(x)) // "something failed in bar with x=-1: <error>"
}
This is nice because I can wrap errors without defer
which can be error prone unless we use named return values or wrap with another func, it is also nice because defer
would add the same error handler for all errors even if I want to handle 2 of them differently. You can also use it as you see fit, for example:
func foo(a, b string) (int64, error) {
return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func withContext(a, b string) (int64, error) {
catch(func (err error) error {
return fmt.Errorf("can't parse a: %s, b: %s, err: %v", a, b, err)
})
return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func moreExplicitContext(a, b string) (int64, error) {
catch(func (err error) error {
return fmt.Errorf("can't parse a: %s, err: %v", a, err)
})
x := try(strconv.Atoi(a))
catch(func (err error) error {
return fmt.Errorf("can't parse b: %s, err: %v", b, err)
})
y := try(strconv.Atoi(b))
return x + y
}
func withHelperWrapf(a, b string) (int64, error) {
catch(wrapf("can't parse a: %s", a))
x := try(strconv.Atoi(a))
catch(wrapf("can't parse b: %s", b))
y := try(strconv.Atoi(b))
return x + y
}
func before(a, b string) (int64, error) {
x, err := strconv.Atoi(a)
if err != nil {
return 0, fmt.Errorf("can't parse a: %s, err: %v", a, err)
}
y, err := strconv.Atoi(b)
if err != nil {
return 0, fmt.Errorf("can't parse b: %s, err: %v", b, err)
}
return x + y
}
And still on the chaotic mood (to help you empathize) If you don't like catch
, you don't have to use it.
Now... I don't really mean it the last sentence, but it does feel like it's not helpful for the discussion, very aggressive IMO.
Still, if we went this route I think that we may as well have try{}catch(error err){}
instead :stuck_out_tongue:
See also #27519 - the #id/catch error model
No one is going to make you use try.
Ignoring the glibness, I think that's a pretty hand-wavy way to dismiss a design criticism.
Sorry, glib was not my intent.
What I'm trying to say, is that try
is not intended to be a 100% solution. There are various error-handling paradigms that are not well-handled by try
. For instance, if you need to add callsite-dependent context to the error. You can always fall back to using if err != nil {
to handle those more complicated cases.
It is certainly a valid argument that try
can't handle X, for various instances of X. But often handling case X means making the mechanism more complicated. There's a tradeoff here, handling X on one hand but complicating the mechanism for everything else. What we do all depends on how common X is, and how much complication it would require to handle X.
So by "No one is going to make you use try", I mean that I think the example in question is in the 10%, not the 90%. That assertion is certainly up for debate, and I'm happy to hear counterarguments. But eventually we're going to have to draw the line somewhere and say "yeah, try
will not handle that case. You'll have to use old-style error handling. Sorry.".
It's not that "try can't handle this specific case of error handling" that's the issue, it's "try encourages you to not wrapping your errors". The check-handle
idea forced you to write a return statement, so writing a error wrapping was pretty trivial.
Under this proposal you need to use a named return with a defer
, which is not intuitive and seems very hacky.
The
check-handle
idea forced you to write a return statement, so writing a error wrapping was pretty trivial.
That isn't true - in the design draft, every function that returns an error has a default handler which just returns the error.
Building on @Goodwine's impish point, you don't really need separate functions like HandleErrorf
if you have a single bridge function like
func handler(err *error, handle func(error) error) {
// nil handle is treated as the identity
if *err != nil && handle != nil {
*err = handle(*err)
}
}
which you would use like
defer handler(&err, func(err error) error {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("oops: %w", err)
})
You could make handler
itself a semi-magic builtin like try
.
If it's magic, it could take its first argument implicitly—allowing it to be used even in functions that don't name their error
return, knocking out one of the less fortunate aspects of the current proposal while making it less fussy and error prone to decorate errors. Of course, that doesn't reduce the previous example by much:
defer handler(func(err error) error {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("oops: %w", err)
})
If it were magic in this way, it would have to be a compile time error if it were used anywhere except as the argument to defer
. You could go a step farther and make it implicitly defer, but defer handler
reads quite nicely.
Since it uses defer
it could call its handle
func whenever there was a non-nil error being returned, making it useful even without try
since you could add a
defer handler(wrapErrWithPackageName)
at the top to fmt.Errorf("mypkg: %w", err)
everything.
That gives you a lot of the older check
/handle
proposal but it works with defer naturally (and explicitly) while getting rid of the need, in most cases, to explicitly name an err
return. Like try
it's a relatively straightforward macro that (I imagine) could be implemented entirely in the front end.
That isn't true - in the design draft, every function that returns an error has a default handler which just returns the error.
My bad, you are correct.
I mean that I think the example in question is in the 10%, not the 90%. That assertion is certainly up for debate, and I'm happy to hear counterarguments. But eventually we're going to have to draw the line somewhere and say "yeah, try will not handle that case. You'll have to use old-style error handling. Sorry.".
Agreed, my opinion is that this line sould be drawn when checking for EOF or similar, not at wrapping. But maybe if errors had more context this wouldn't be an issue anymore.
Could try()
auto-wrap errors with useful context for debugging? E.g. if xerrors
becomes errors
, errors should have a something that looks like a stack trace that try()
could add, no? If so maybe that would be enough 🤔
If the goals are (reading https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md) :
I would take the suggestion give it an angle and allow "small steps" code migration for all the billions lines of code out there.
instead of the suggested:
func printSum(a, b string) error {
defer fmt.HandleErrorf(&err, "sum %s %s: %v", a,b, err)
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x + y)
return nil
}
We can:
func printSum(a, b string) error {
var err ErrHandler{HandleFunc : twoStringsErr("printSum",a,b)}
x, err.Error := strconv.Atoi(a)
y,err.Error := strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
What would we gain?
twoStringsErr can be inlined to printSum, or a general handler that knows how to capture errors (in this case with 2 string parameters) - so if I have same repeating func signatures used in many of my functions, I dont need t rewrite the handler each time
in the same manner, I can have the ErrHandler type extended in the manner of:
type ioErrHandler ErrHandler
func (i ErrHandler) Handle() ...{
}
or
type parseErrHandler ErrHandler
func (p parseErrHandler) Handle() ...{
}
or
type str2IntErrHandler ErrHandler
func (s str2IntErrHandler) Handle() ...{
}
and use this all around my my code:
func printSum(a, b string) error {
var pErr str2IntErrHandler
x, err.Error := strconv.Atoi(a)
y,err.Error := strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
So, the actual need would be to develop a trigger when err.Error is set to not nil
Using this method we can also:
func (s str2IntErrHandler) Handle() bool{
**return false**
}
Which would tell the calling function to continue instead of return
And use different error handlers in the same function:
func printSum(a, b string) error {
var pErr str2IntErrHandler
var oErr overflowError
x, err.Error := strconv.Atoi(a)
y,err.Error := strconv.Atoi(b)
fmt.Println("result:", x + y)
totalAsByte,oErr := sumBytes(x,y)
sunAsByte,oErr := subtractBytes(x,y)
return nil
}
etc.
Going over the goals again
x, err := strconv.Atoi(a)
to
x, err.Error := strconv.Atoi(a)
and actually - better readability (IMO, again)
@guybrand you are the latest adherent to this recurring theme (which I like).
See https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes
@guybrand That seems like an entirely different proposal; I think you should file it as its own issue so that this one can focus on discussing @griesemer's proposal.
@natefinch agree. I think this is more geared towards improving the experience while writing Go instead of optimizing for reading. I wonder if IDE macros or snippets could solve the issue without this becoming a feature of the language.
@Goodwine
Let's say it's a good practice to wrap errors with useful context,
try()
would be considered a bad practice because it's not adding any context. This means thattry()
is a feature nobody wants to use and become a feature that's used so rarely that it may as well not have existed.
As noted in the proposal (and shown by example), try
doesn't fundamentally prevent you from adding context. I'd say that the way it is proposed, adding context to errors is entirely orthogonal to it. This is addressed specifically in the FAQ of the proposal.
I recognize that try
won't be useful if within a single function if there are a multitude of different contexts that you want to add to different errors from function calls. However, I also believe that something in the general vein of HandleErrorf
covers a large area of use because only adding function-wide context to errors is not unusual.
Instead of just saying "well, if you don't like it, don't use it and shut up" (this is how it reads), I think it would be better to try to address what many of use are considering a flaw in the design.
If that is how it reads I apologize. My point isn't that you should pretend that it doesn't exist if you don't like it. It's that it's obvious that there are cases in which try
would be useless and that you shouldn't use it in such cases, which for this proposal I believe strikes a good balance between KISS and general utility. I didn't think that I was unclear on that point.
Thanks everybody for the prolific feedback so far; this is very informative.
Here's my attempt at an initial summary, to get a better feeling for the feedback. Apologies in advance for anybody I have missed or misrepresented; I hope that I got the overall gist of it right.
0) On the positive side, @rasky, @adg, @eandre, @dpinela, and others explicitly expressed happiness over the code simplification that try
provides.
1) The most important concern appears to be that try
does not encourage good error handling style but instead promotes the "quick exit". (@agnivade, @peterbourgon, @politician, @a8m, @eandre, @prologic, @kungfusheep, @cpuguy, and others have voiced their concern about this.)
2) Many people don't like the idea of a built-in, or the function syntax that comes with it because it hides a return
. It would be better to use a keyword. (@sheerun, @Redundancy, @dolmen, @komuw, @RobertGrantEllis, @elagergren-spideroak). try
may also be easily overlooked (@peterbourgon), especially because it can appear in expressions that may be arbitrarily nested. @natefinch is concerned that try
makes it "too easy to dump too much in one line", something that we usually try to avoid in Go. Also, IDE support to emphasize try
may not be sufficient (@dominikh); try
needs to "stand on its own".
3) For some, the status quo of explicit if
statements is not a problem, they are happy with it (@bitfield, @marwan-at-work, @natefinch). It's better to have only one way to do things (@gbbr); and explicit if
statements are better than implicit return
's (@DavexPro, @hmage, @prologic, @natefinch).
Along the same lines, @mattn is concerned about the "implicit binding" of the error result to try
- the connection is not explicitly visible in the code.
4) Using try
will make it harder to debug code; for instance, it may be necessary to rewrite a try
expression back into an if
statement just so that debugging statements can be inserted (@deanveloper, @typeless, @networkimprov, others).
5) There's some concern about the use of named returns (@buchanae, @adg).
Several people have provided suggestions to improve or modify the proposal:
6) Some have picked up on the idea of an optional error handler (@beoran) or format string provided to try
(@unexge, @a8m, @eandre, @gotwarlost) to encourage good error handling.
7) @pierrec suggested that gofmt
could format try
expressions suitably to make them more visible.
Alternatively, one could make existing code more compact by allowing gofmt
to format if
statements checking for errors on one line (@zeebo).
8) @marwan-at-work argues that try
simply shifts error handling from if
statements to try
expressions. Instead, if we want to actually solve the problem, Go should "own" error handling by making it truly implicit. The goal should be to make (proper) error handling simpler and developers more productive (@cpuguy).
9) Finally, some people don't like the name try
(@beoran, @HiImJC, @dolmen) or would prefer a symbol such as ?
(@twisted1919, @leaxoy, others).
Some comments on this feedback (numbered accordingly):
0) Thanks for the positive feedback! :-)
1) It would be good to learn more about this concern. The current coding style using if
statements to test for errors is about as explicit as it can be. It's very easy to add additional information to an error, on an individual basis (for each if
). Often it makes sense to handle all errors detected in a function in a uniform way, which can be done with a defer
- this is already possible now. It is the fact that we already have all the tools for good error handling in the language, and the problem of a handler construct not being orthogonal to defer
, that led us to leave away a new mechanism solely for augmenting errors.
2) There is of course the possibility to use a keyword or special syntax instead of a built-in. A new keyword will not be backward-compatible. A new operator might, but seems even less visible. The detailed proposal discusses the various pros and cons at length. But perhaps we are misjudging this.
3) The reason for this proposal is that error handling (specifically the associated boilerplate code) was mentioned as a significant issue in Go (next to the lack of generics) by the Go community. This proposal directly addresses the boilerplate concern. It does not do more than solve the most basic case because any more complex case is better handled with what we already have. So while a good number of people are happy with the status quo, there is a (probably) equally large contingent of people that would love a more streamlined approach such as try
, well-knowing that this is "just" syntactic sugar.
4) The debugging point is a valid concern. If there's a need to add code between detecting an error and a return
, having to rewrite atry
expression into an if
statement could be annoying.
5) Named return values: The detailed document discusses this at length. If this is the main concern about this proposal then we're in a good spot, I think.
6) Optional handler argument to try
: The detailed document discusses this as well. See the section on Design iterations.
7) Using gofmt
to format try
expressions such that they are extra visible would certainly be an option. But it would take away from some of the benefits of try
when used in an expression.
8) We have considered looking at the problem from the error handling (handle
) point of view rather than from the error testing (try
) point of view. Specifically, we briefly considered only introducing the notion of an error handler (similar to the original design draft presented at last year's Gophercon). The thinking was that if (and only if) a handler is declared, in multi-value assignments where the last value is of type error
, that value can simply be left away in an assignment. The compiler would implicitly check if it is non-nil, and if so branch to the handler. That would make explicit error handling disappear completely and encourage everybody to write a handler instead. This seemed to extreme an approach because it would be completely implicit - the fact that a check happens would be invisible.
9) May I suggest that we don't bike-shed the name at this point. Once all the other concerns are settled is a better time to fine-tune the name.
This is not to say that the concerns are not valid - the replies above are simply stating our current thinking. Going forward, it would be good to comment on new concerns (or new evidence in support of these concerns) - just restating what has been said already does not provide us with more information.
And finally, it appears that not everybody commenting on the issue has read the detailed doc. Please do so before commenting to avoid repeating what has already been said. Thanks.
This is not a comment on the proposal, but a typo report. It wasn't fixed since the full proposal was published, so I thought I'd mention it:
func try(t1 T1, t1 T2, … tn Tn, te error) (T1, T2, … Tn)
should be:
func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)
Would it be worth analyzing openly available Go code for error checking statements to try and figure out if most error checks are truly repetitive or if in most cases, multiple checks within the same function add different contextual information? The proposal would make a lot of sense for the former case but wouldn't help the latter. In the latter case, people will either continue using if err != nil
or give up on adding extra context, use try()
and resort to adding common error context per function which IMO would be harmful. With upcoming error values features, I think we expect people wrap errors with more info more often. Probably I misunderstood the proposal but AFAIU, this helps reduce the boilerplate only when all errors from a single function must be wrapped in exactly one way and doesn't help if a function deals with five errors that might need to be wrapped differently. Not sure how common such cases in the wild (pretty common in most of my projects) are but I'm concerned try()
might encourage people to use common wrappers per function even when it'd make sense to wrap different errors differently.
Just a quick comment backed with data of a small sample set:
We propose a new built-in function called try, designed specifically to eliminate the boilerplate if statements typically associated with error handling in Go
Iff this is the core problem being solved by this proposal, I find that this "boilerplate" only accounts for ~1.4% of my code across dozens of publically available open source projects totalling ~60k SLOC.
Curious if anyone else has similar stats?
On a much larger codebase like Go itself totalling around ~1.6M SLOC this amounts to about ~0.5% of the codebase having lines like if err != nil
.
Is this really the most impactful problem to solve with Go 2?
Thank you very much @griesemer for taking the time to go through everyone's ideas and explicitly providing thoughts. I think that it really helps with the perception that the community is being heard in the process.
- @pierrec suggested that gofmt could format try expressions suitably to make them more visible.
Alternatively, one could make existing code more compact by allowing gofmt to format if statements checking for errors on one line (@zeebo).
- Using
gofmt
to formattry
expressions such that they are extra visible would certainly be an option. But it would take away from some of the benefits oftry
when used in an expression.
These are valuable thoughts about requiring gofmt
to format try
, but I'm interested if there are any thoughts in particular on gofmt
allowing the if
statement checking the error to be one line. The proposal was lumped in with formatting of try
, but I think it's a completely orthogonal thing. Thanks.
@griesemer thank you for the incredible work going through all the comments and answering most if not all of the feedback 🎉
One thing that was not addressed in your feedback was the idea of use the tooling/vetting part of the Go language to improve the error handling experience, rather then updating the Go syntax.
For example, with the landing of the new LSP (gopls
), it seems like a perfect place to analyze a function's signature and taking care of the error handling boilerplate for the developer, with proper wrapping and vetting too.
@griesemer I'm sure that this is not well thought through, but I tried to modify your suggestion closer to something that I'd be comfortable with here: https://www.reddit.com/r/golang/comments/bwvyhe/proposal_a_builtin_go_error_check_function_try/eq22bqa?utm_source=share&utm_medium=web2x
@zeebo It would be easy to make gofmt
format if err != nil { return ...., err }
on a single line. Presumably it would only be for this specific kind of if
pattern, not all "short" if
statements?
Along the same lines, there were concerns about try
being invisible because it's on the same line as the business logic. We have all these options:
Current style:
a, b, c, ... err := BusinessLogic(...)
if err != nil {
return ..., err
}
One-line if
:
a, b, c, ... err := BusinessLogic(...)
if err != nil { return ..., err }
try
on a separate line (!):
a, b, c, ... err := BusinessLogic(...)
try(err)
try
as proposed:
a, b, c := try(BusinessLogic(...))
The first and the last line seem the clearest (to me), especially once one is used to recognize try
as what it is. With the last line, an error is explicitly checked for, but since it's (usually) not the main action, it is a bit more in the background.
@marwan-at-work I'm not sure what you are proposing that the tools are do for you. Do you suggest that they hide the error handling somehow?
@dpinela
@guybrand That seems like an entirely different proposal; I think you should file it as its own issue so that this one can focus on discussing @griesemer's proposal.
IMO my proposal differs only in syntax, meaning:
so the main diff is whether we're wrapping the original function call with try(func()) that would always analyze the last var to jnz the call or use the actual return value to do that.
I know it looks diff, but actually very similar in concept.
On the other hand - if you take the usual try .... catch in many c-like languages - that would be a very different implementation, different readability etc.
I do however seriously think of writing a proposal, thanks for the idea.
@griesemer
I'm not sure what you are proposing that the tools are do for you. Do you suggest that they hide the error handling somehow?
Quite the opposite: I'm suggesting that gopls
can optionally write the error handling boilerplate for you.
As you mentioned in your last comment:
The reason for this proposal is that error handling (specifically the associated boilerplate code) was mentioned as a significant issue in Go (next to the lack of generics) by the Go community
So the heart of the problem is that the programmer ends up writing a lot of boilerplate code. So the issue is about writing, not reading. Therefore, my suggestion is: let the computer (tooling/gopls) do the writing for the programmer by analyzing the function signature and placing proper error handling clauses.
For example:
// user begins to write this function:
func openFile(path string) ([]byte, error) {
file, err := os.Open(path)
defer file.Close()
bts, err := ioutil.ReadAll(file)
return bts, nil
}
Then the user triggers the tool, perhaps by just saving the file (similar to how gofmt/goimports typically work) and gopls
would look at this function, analyze its return signature and augments the code to be this:
// user has triggered the tool (by saving the file, or code action)
func openFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("openFile: %w", err)
}
defer file.Close()
bts, err := ioutil.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("openFile: %w", err)
}
return bts, nil
}
This way, we get the best of both worlds: we get the readability/explicitness of the current error handling system, and the programmer did not write any error handling boilerplate. Even better, the user can go ahead and modify the error handling blocks later on to do different behavior: gopls
can understand that the block exists, and it wouldn't modify it.
How would the tool know that I intended to handle the err
later on in the function instead of returning early? Albeit rare, but code I have written nonetheless.
I apologize if this has been brought up before, but I couldn't find any mention of it.
try(DoSomething())
reads well to me, and makes sense: the code is trying to do something. try(err)
, OTOH, feels a little off, semantically speaking: how does one try an error? In my mind, one could _test_ or _check_ an error, but _trying_ one doesn't seem right.
I do realize that allowing try(err)
is important for reasons of consistency: I suppose it would be strange if try(DoSomething())
worked, but err := DoSomething(); try(err)
didn't. Still, it feels like try(err)
looks a little awkward on the page. I can't think of any other built-in functions that can be made to look this strange so easily.
I do not have any concrete suggestions on the matter, but I nevertheless wanted to make this observation.
@griesemer Thanks. Indeed, the proposal was to only be for return
, but I suspect allowing any single statement to be a single line would be good. For example in a test one could, with no changes to the testing library, have
if err != nil { t.Fatal(err) }
The first and the last line seem the clearest (to me), especially once one is used to recognize try as what it is. With the last line, an error is explicitly checked for, but since it's (usually) not the main action, it is a bit more in the background.
With the last line, some of the cost is hidden. If you want to annotate the error, which I believe the community has vocally said is desired best practice and should be encouraged, one would have to change the function signature to name the arguments and hope that a single defer
applied to every exit in the function body, otherwise try
has no value; perhaps even negative due to its ease.
I don't have any more to add that I believe hasn't already been said.
I didn't see how to answer this question from the design doc. What does this code do:
func foo() (err error) {
src := try(getReader())
if src != nil {
n, err := src.Read(nil)
if err == io.EOF {
return nil
}
try(err)
println(n)
}
return nil
}
My understanding is that it would desugar into
func foo() (err error) {
tsrc, te := getReader()
if err != nil {
err = te
return
}
src := tsrc
if src != nil {
n, err := src.Read(nil)
if err == io.EOF {
return nil
}
terr := err
if terr != nil {
err = terr
return
}
println(n)
}
return nil
}
which fails to compile because err
is shadowed during a naked return. Would this not compile? If so, that's a very subtle failure, and doesn't seem too unlikely to happen. If not, then more is going on than some sugar.
@marwan-at-work
As you mentioned in your last comment:
The reason for this proposal is that error handling (specifically the associated boilerplate code) was mentioned as a significant issue in Go (next to the lack of generics) by the Go community
So the heart of the problem is that the programmer ends up writing a lot of boilerplate code. So the issue is about writing, not reading.
I think it's actually the other way round - for me the biggest annoyance with the current error handling boilerplate isn't so much having to type it, but rather how it scatters the function's happy path vertically across the screen, making it harder to understand at a glance. The effect is particularly pronounced in I/O-heavy code, where there's usually a block of boilerplate between every two operations. Even a simplistic version of CopyFile
takes ~20 lines even though it really only performs five steps: open source, defer close source, open destination, copy source -> destination, close destination.
Another issue with the current syntax, is that, as I noted earlier, if you have a chain of operations each of which can return an error, the current syntax forces you to give names to all the intermediate results, even if you'd rather leave some anonymous. When this happens, it also hurts readability because you have to spend brain cycles parsing those names, even though they're not very informative.
I like try
on separate line.
And I hope that it can specify handler
func independently.
func try(error, optional func(error)error)
func (p *pgStore) DoWork() error {
tx, err := p.handle.Begin()
try(err)
handle := func(err error) error {
tx.Rollback()
return err
}
var res int64
_, err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
try(err, handle)
_, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
try(err, handle)
return tx.Commit()
}
@zeebo: The examples I gave are 1:1 translations. The first (traditional if
) didn't handle the error, and so didn't the others. If the first handled the error, and if this were the only place an error is checked for in a function, the first example (using an if
) might be the appropriate choice of writing the code. If there's multiple error checks, all of which use the same error handling (wrapping), say because they all add information about the current function, one could use a defer
statement to handle the errors all in one place. Optionally, one could rewrite the if
's into try
's (or leave them alone). If there are multiple errors to check, and they all handle the errors differently (which might be a sign that the function's concern is too broad and that it might need to be split up), using if
's is the way to go. Yes, there is more than one way to do the same thing, and the right choice depends on the code as well as on personal taste. While we do strive in Go for "one way to do one thing", this is of course already not the case, especially for common constructs. For instance, when an if
-else
-if
sequence becomes too long, sometimes a switch
might be more appropriate. Sometimes a variable declaration var x int
expresses intent better than x := 0
, and so forth (though not everybody is happy about this).
Regarding your question about the "rewrite": No, there wouldn't be a compilation error. Note that the rewrite happens internally (and may be more efficient than the code pattern suggest), and there is no need for the compiler to complain about a shadowed return. In your example, you declared a local err
variable in a nested scope. try
would still have direct access to the result err
variable, of course. The rewrite might look more like this under the covers.
[edited] PS: A better answer would be: try
is not a naked return (even though the rewrite looks like it). After all one explicitly gives try
an argument that contains (or is) the error that is returned if not nil
. The shadow error for naked returns is an error on the source (not the underlying translation of the source. The compiler doesn't need the error.
If the overarching function's final return type is not of type error, can we panic?
It will make the builtin more versatile (such as satisfy my concern in #32219)
@pjebs This has been considered and decided against. Please read the detailed design doc (which explicitly refers to your issue on this subject).
I also want to point that try() is treated as expression eventhough it works as return statement. Yes, I know try is builtin macro but most of users will use this like functional programming, I guess.
func doSomething() (error, error, error, error, error) {
...
}
try(try(try(try(try(doSomething)))))
The design says you explored using panic
instead of returning with the error.
I am highlighting a subtle difference:
Do exactly what your current proposal states, except remove restriction that overarching function must have a final return type of type error
.
If it doesn't have a final return type of error
=> panic
If using try for package level variable declarations => panic (removes need for MustXXX( )
convention)
For unit tests, a modest language change.
@mattn, I highly doubt any signficant number of people will write code like that.
@pjebs, that semantics - panic if there's no error result in the current function - is exactly what the design doc is discussing in https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md#discussion.
Furthermore, in an attempt to make try useful not just inside functions with an error result, the semantics of try depended on the context: If try were used at the package-level, or if it were called inside a function without an error result, try would panic upon encountering an error. (As an aside, because of that property the built-in was called must rather than try in that proposal.) Having try (or must) behave in this context-sensitive way seemed natural and also quite useful: It would allow the elimination of many user-defined must helper functions currently used in package-level variable initialization expressions. It would also open the possibility of using try in unit tests via the testing package.
Yet, the context-sensitivity of try was considered fraught: For instance, the behavior of a function containing try calls could change silently (from possibly panicking to not panicking, and vice versa) if an error result was added or removed from the signature. This seemed too dangerous a property. The obvious solution would have been to split the functionality of try into two separate functions, must and try (very similar to what is suggested by issue #31442). But that would have required two new built-in functions, with only try directly connected to the immediate need for better error handling support.
@pjebs That is _exactly_ what we considered in a prior proposal (see detailed doc, section on Design iterations, 4th paragraph):
Furthermore, in an attempt to make try useful not just inside functions with an error result, the semantics of try depended on the context: If try were used at the package-level, or if it were called inside a function without an error result, try would panic upon encountering an error. (As an aside, because of that property the built-in was called must rather than try in that proposal.)
The (Go Team internal) consensus was that it would be confusing for try
to depend on context and act so differently. For instance, adding an error result to a function (or remove it) could silently change the behavior of the function from panicking to not panicking (or vice versa).
@griesemer Thanks for the clarification about the rewrite. I'm glad that it will compile.
I understand the examples were translations that didn't annotate the errors. I attempted to argue that try
makes it harder to do good annotation of errors in common situations, and that error annotation is very important to the community. A large portion of the comments thus far have been exploring ways to add better annotation support to try
.
About having to handle the errors differently, I disagree that it's a sign that the function's concern is too broad. I've been translating some examples of claimed real code from the comments and placing them in a dropdown at the bottom of my original comment, and the example in https://github.com/golang/go/issues/32437#issuecomment-499007288 I think demonstrates a common case well:
func (c *Config) Build() error {
pkgPath, err := c.load()
if err != nil { return nil, errors.WithMessage(err, "load config dir") }
b := bytes.NewBuffer(nil)
err = templates.ExecuteTemplate(b, "main", c)
if err != nil { return nil, errors.WithMessage(err, "execute main template") }
buf, err := format.Source(b.Bytes())
if err != nil { return nil, errors.WithMessage(err, "format main template") }
target := fmt.Sprintf("%s.go", filename(pkgPath))
err = ioutil.WriteFile(target, buf, 0644)
if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
// ...
}
That function's purpose is to execute a template on some data into a file. I don't believe it needs to be split up, and it would be unfortunate if all of those errors just gained the line that they were created on from a defer. That may be alright for developers, but it's much less useful for users.
I think it's also a bit of a signal how subtle the defer wrap(&err, "message: %v", err)
bugs were and how they tripped up even experienced Go programmers.
To summarize my argument: I think error annotation is more important than expression based error checking, and we can get quite a bit of noise reduction by allowing statement based error checking to be one line instead of three. Thanks.
@griesemer sorry I read a different section which discussed panic and didn't see the discussion of the dangers.
@zeebo Thanks for this example. It looks like using an if
statement is exactly the right choice in this case. But point taken, formatting the if's into one-liners may streamline this a bit.
I would like to bring up once again the idea of a handler as a second argument to try
, but with the addition that the handler argument be _required_, but nil-able. This makes handling the error the default, instead of the exception. In cases where you really do want to pass the error up unchanged, simply provide a nil value to the handler and try
will behave just like in the original proposal, but the nil argument will act as a visual cue that the error is not being handled. It will be easier to catch during code review.
file := try(os.Open("my_file.txt"), nil)
What should happen if the handler is provided but is nil? Should try panic or treat it as an absent error handler?
As mentioned above, try
will behave in accordance with the original proposal. There would be no such thing as an absent error handler, only a nil one.
What if the handler is invoked with a non-nil error and then returns a nil result? Does this mean the error is “cancelled”? Or should the enclosing function return with a nil error?
I believe that the enclosing function would return with a nil error. It would potentially be very confusing if try
could sometimes continue execution even after it received a non-nil error value. This would allow for handlers to "take care" of the error in some circumstances. This behavior could be useful in a "get or create" style function, for example.
func getOrCreateObject(obj *object) error {
defaultObjectHandler := func(err error) error {
if err == ObjectDoesNotExistErr {
*obj = object{}
return nil
}
return fmt.Errorf("getting or creating object: %v", err)
}
*obj = try(db.getObject(), defaultObjectHandler)
}
It was also not clear if permitting an optional error handler would lead programmers to ignore proper error handling altogether. It would also be easy to do proper error handling everywhere but miss a single occurrence of a try. And so forth.
I believe that both of these concerns are alleviated by making the handler a required, nil-able argument. It requires programmers to make a conscious, explicit decision that they will not handle their error.
As a bonus, I think that requiring the error handler also discourages deeply nested try
s because they are less brief. Some might see this as a downside, but I think it's a benefit.
@velovix I love the idea, but why does error-handler have to be required? Can't it be nil
by default? Why do we need a "visual clue"?
@griesemer What if @velovix idea was adopted but with builtin
containing a predefined function that converts err to panic AND We remove requirement that over-arching function have an error return value?
The idea is, if the overarching function does not return error, using try
without error-handler is a compile-time error.
The error-handler can also be used to wrap the soon-to-be-returned error using various libraries etc at the location of error, instead of a defer
at the top that modifies a named returned error.
@pjebs
why does error-handler have to be required? Can't it be nil by default? Why do we need a "visual clue"?
This is to address the concerns that
try
proposal as it is now could discourage people from providing context to their errors because doing so isn't quite so straightforward.Having a handler in the first place makes providing context easier, and having the handler be a required argument sends a message: The common, recommended case is to handle or contextualize the error in some way, not simply pass it up the stack. It's in line with the general recommendation from the Go community.
It was also not clear if permitting an optional error handler would lead programmers to ignore proper error handling altogether. It would also be easy to do proper error handling everywhere but miss a single occurrence of a try. And so forth.
Having to pass an explicit nil
makes it more difficult to forget to handle an error properly. You have to explicitly decide to not handle the error instead of doing so implicitly by leaving out an argument.
Thinking further about the conditional return briefly mentioned at https://github.com/golang/go/issues/32437#issuecomment-498947603.
It seems
return if f, err := os.Open("/my/file/path"); err != nil
would be more compliant with how Go's exising if
looks.
If we add a rule for the return if
statement that
when the last condition expression (like err != nil
) is not present,
and the last variable of the declaration in the return if
statement is of the type error
,
then the value of the last variable will be automatically compared with nil
as the implicit condition.
Then the return if
statement can be abbreviated into:
return if f, err := os.Open("my/file/path")
Which is very close to the signal-noise ratio that the try
provides.
If we change the return if
to try
, it becomes
try f, err := os.Open("my/file/path")
It again becomes similar to other proposed variations of the try
in this thread, at least syntactically.
Personally, I still prefer return if
over try
in this case because it makes the exit points of a function very explicit. For instance, when debugging I often highlight the keyword return
within the editor to identify all exit points of a large function.
Unfortunately, it doesn't seem to help enough with the inconvenience of inserting debug logging either.
Unless we also allow a body
block for return if
, like
Original:
return if f, err := os.Open("my/path")
When debugging:
- return if f, err := os.Open("my/path")
+ return if f, err := os.Open("my/path") {
+ fmt.Printf("DEBUG: os.Open: %s\n", err)
+ }
The meaning of the body block of return if
is obvious, I assume. It will be executed before defer
and return.
That said, I don't have complaints with the existing error-handling approach in Go.
I am more concerned about how the addition of the new error-handling would impact the present goodness of Go.
@velovix We quite liked the idea of a try
with an explicit handler function as 2nd argument. But there were too many questions that didn't have obvious answers, as the design doc states. You have answered some of them in a way that seems reasonable to you. It's quite likely (and that was our experience inside the Go Team), that somebody else thinks the correct answer is quite a different one. For instance, you are stating that the handler argument should always be provided, but that it can be nil
, to make it explicit we don't care about handling the error. Now what happens if one provides a function value (not a nil
literal), and that function value (stored in a variable) happens to be nil? By analogy with the explicit nil
value, no handling is required. But others might argue that this is a bug in the code. Or, alternatively one could allow nil-valued handler arguments, but then a function might inconsistently handler errors in some cases and not in others, and it's not necessarily obvious from the code which one do, because it appears as if a handler is always present. Another argument was that it's better to have a top-level declaration of an error handler because that makes it very clear that the function does handle errors. Hence the defer
. There's probably more.
It would be good to learn more about this concern. The current coding style using if statements to test for errors is about as explicit as it can be. It's very easy to add additional information to an error, on an individual basis (for each if). Often it makes sense to handle all errors detected in a function in a uniform way, which can be done with a defer - this is already possible now. It is the fact that we already have all the tools for good error handling in the language, and the problem of a handler construct not being orthogonal to defer, that led us to leave away a new mechanism solely for augmenting errors.
@griesemer - IIUC, you are saying that for callsite-dependent error contexts, the current if statement is fine. Whereas, this new try
function is useful for the cases where handling multiple errors at a single place is useful.
I believe the concern was that, while simply doing a if err != nil { return err}
may be fine for some cases, it is usually recommended to decorate the error before returning. And this proposal seems to address the previous and does not do much for the latter. Which essentially means folks will be encouraged to use easy-return pattern.
@agnivade You are correct, this proposal does exactly nothing to help with error decoration (but to recommend the use of defer
). One reason is that language mechanisms for this already exist. As soon as error decoration is required, especially on an individual error basis, the additional amount of source text for the decoration code makes the if
less onerous in comparison. It's the cases where no decoration is required, or where the decoration is always the same, where the boilerplate becomes a visible nuisance and then detracts from the important code.
Folks are already encouraged to use an easy-return pattern, try
or no try
, there's just less to write. Come to think of it, _the only way to encourage error decoration is to make it mandatory_, because no matter what language support is available, decorating errors will require more work.
One way to sweeten the deal would be to only permit something like try
(or any analogous shortcutting notation) _if_ an explicit (possibly empty) handler is provided somewhere (note that the original draft design didn't have such a requirement, either).
I'm not sure we want to go so far. Let me restate that plenty of perfectly fine code, say internals of a library, does not need to decorate errors everywhere. It's fine to just propagate errors up and decorate them just before they leave the API entry points, for instance. (In fact, decorating them everywhere will only lead to overdecorated errors that, with the real culprits hidden, make it harder to locate the important errors; very much like overly verbose logging can make it difficult to see what's really going on).
I think we can also add a catch function, which would be a nice pair, so:
func a() int {
x := randInt()
// let's assume that this is what recruiters should "fix" for us
// or this happens in 3rd-party package.
if x % 1337 != 0 {
panic("not l33t enough")
}
return x
}
func b() error {
// if a() panics, then x = 0, err = error{"not l33t enough"}
x, err := catch(a())
if err != nil {
return err
}
sendSomewhereElse(x)
return nil
}
// which could be simplified even further
func c() error {
x := try(catch(a()))
sendSomewhereElse(x)
return nil
}
in this example, catch()
would recover()
a panic and return ..., panicValue
.
of course, we have an obvious corner case in which we have a func, that also returns an error. in this case I think it would be convenient to just pass-thru error value.
so, basically, you can then use catch() to actually recover() panics and turn them into errors.
this looks quite funny for me, 'cause Go doesn't actually have exceptions, but in this case we have pretty neat try()-catch() pattern, that also shouldn't blow up your entire codebase with something like Java (catch(Throwable)
in Main + throws LiterallyAnything
). you can easily process someone's panics like those was usual errors. I've currently have about 6mln+ LoC in Go in my current project, and I think this would simplify things at least for me.
@griesemer Thanks for your recap of the discussion.
I notice one point is missing in there: some people have argued that we should wait with this feature until we have generics, which will hopefully allow us to solve this problem in a more elegant way.
Furthermore, I also like @velovix's suggestion, and while I appreciate that this raises a few questions as described in the spec, I think these can be easily answered in a reasonable way, as @velovix already did.
For example:
What happens if one provides a function value (not a nil literal), and that function value (stored in a variable) happens to be nil? => Do not handle the error, period. This is useful in case the error handling depends on context and the handler variable is set depending on whether or not error handling is required. It's not a bug, rather, it's a feature. :)
Another argument was that it's better to have a top-level declaration of an error handler because that makes it very clear that the function does handle errors. => So define the error handler at the top of the function as a named closure function and use that, so it's also very clear that the error should be handled. This is not a serious issue, more of a style requirement.
What other concerns were there? I am pretty sure they can be all be answered similarly in a reasonable way.
Finally, as you say, "one way to sweeten the deal would be to only permit something like try (or any analogous shortcutting notation) if an explicit (possibly empty) handler is provided somewhere". I think that Iif we are to proceed with this proposal, we should actually take it "this far", to encourage proper, "explicit is better than implicit" error handling.
@griesemer
Now what happens if one provides a function value (not a nil literal), and that function value (stored in a variable) happens to be nil? By analogy with the explicit nil value, no handling is required. But others might argue that this is a bug in the code.
In theory this does seem like a potential gotcha, though I'm having a hard time conceptualizing a reasonable situation where a handler would end up being nil by accident. I imagine that handlers would most commonly either come from a utility function defined elsewhere, or as a closure defined in the function itself. Neither of these are likely to become nil unexpectedly. You could theoretically have a scenario where handler functions are being passed around as arguments to other functions, but to my eyes it seems rather far-fetched. Perhaps there's a pattern like this that I'm not aware of.
Another argument was that it's better to have a top-level declaration of an error handler because that makes it very clear that the function does handle errors. Hence the
defer
.
As @beoran mentioned, defining the handler as a closure near the top of the function would look very similar in style, and that's how I personally expect people would be using handlers most commonly. While I do appreciate the clarity won by the fact that all functions that handle errors will be using defer
, it may become less clear when a function needs to pivot in its error handling strategy halfway down the function. Then, there will be two defer
s to look at and the reader will have to reason about how they will interact with each other. This is a situation where I believe a handler argument would be both more clear and ergonomic, and I do think that this will be a _relatively_ common scenario.
Is it possible to make it work without brackets?
I.e. something like:
a := try func(some)
@Cyberax - As already mentioned above, it is very essential that you read the design doc carefully before posting. Since this is a high-traffic issue, with a lot of people subscribed.
The doc discusses operators vs functions in detail.
I like this a lot more than I liked august version.
I think that much of the negative feedback, that isn't outright opposed to returns without the return
keyword, can be summarized in two points:
See for example:
The rebuttal for those two objections is respectively:
try
" / it's not going to be appropriate for 100% of casesI don't really have anything to say about 1 (I don't feel strongly about it). But regarding 2 I'd note that the august proposal didn't have this problem, most counter proposals also don't have this problem.
In particular neither the tryf
counter-proposal (that's been posted independently twice in this thread) nor the try(X, handlefn)
counter-proposal (that was part of the design iterations) had this problem.
I think it's hard to argue that try
, as it, is will push people away from decorating errors with relavant context and towards a single generic per-function error decoration.
Because of these reasons I think it's worth trying to address this issue and I want to propose a possible solution:
defer
can only be a function or method call. Allow defer
to also have a function name or a function literal, i.e.defer func(...) {...}
defer packageName.functionName
When panic or deferreturn encounter this type of defer they will call the function passing the zero value for all their parameters
Allow try
to have more than one parameter
When try
encounters the new type of defer it will call the function passing a pointer to the error value as the first parameter followed by all of try
's own parameters, except the first one.
For example, given:
func errorfn() error {
return errors.New("an error")
}
func f(fail bool) {
defer func(err *error, a, b, c int) {
fmt.Printf("a=%d b=%d c=%d\n", a, b, c)
}
if fail {
try(errorfn, 1, 2, 3)
}
}
the following will happen:
f(false) // prints "a=0 b=0 c=0"
f(true) // prints "a=1 b=2 c=3"
The code in https://github.com/golang/go/issues/32437#issuecomment-499309304 by @zeebo could then be rewritten as:
func (c *Config) Build() error {
defer func(err *error, msg string, args ...interface{}) {
if *err == nil || msg == "" {
return
}
*err = errors.WithMessagef(err, msg, args...)
}
pkgPath := try(c.load(), "load config dir")
b := bytes.NewBuffer(nil)
try(templates.ExecuteTemplate(b, "main", c), "execute main template")
buf := try(format.Source(b.Bytes()), "format main template")
target := fmt.Sprintf("%s.go", filename(pkgPath))
try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)
// ...
}
And defining ErrorHandlef as:
func HandleErrorf(err *error, format string, args ...interface{}) {
if *err != nil && format != "" {
*err = fmt.Errorf(format + ": %v", append(args, *err)...)
}
}
would give everyone the much sought after tryf
for free, without pulling fmt
-style format strings into the core language.
This feature is backwards compatible because defer
doesn't allow function expressions as its argument. It doesn't introduce new keywords.
The changes that need to be made to implement it, in addition to the ones outlined in https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md, are:
try
match the signature of the functions passed to defer
try
to copy its arguments into the arguments of the deferred call when it encounters a call deferred by the new kind of defer.After the complexities of the check/handle
draft design, I was pleasantly surprised to see this much simpler and pragmatic proposal land though I'm disappointed that there has been so much push-back against it.
Admittedly a lot of the push-back is coming from people who are quite happy with the present verbosity (a perfectly reasonable position to take) and who presumably wouldn't really welcome any proposal to alleviate it. For the rest of us, I think this proposal hits the sweet spot of being simple and Go-like, not trying to do too much and dove-tailing well with the existing error handling techniques on which you could always fall back if try
didn't do exactly what you wanted.
Regarding some specific points:
The only thing I dislike about the proposal is the need to have a named error return parameter when defer
is used but, having said that, I can't think of any other solution which wouldn't be at odds with the way the rest of the language works. So I think we will just have to accept this if the proposal is adopted.
It's a pity that try
doesn't play well with the testing package for functions which don't return an error value. My own preferred solution to this would be to have a second built-in function (perhaps ptry
or must
) which always panicked rather than returned on encountering a non-nil error and which could therefore be used with the aforementioned functions (including main
). Although this idea has been rejected in the present iteration of the proposal, I formed the impression it was a 'close call' and it may therefore be eligible for reconsideration.
I think it would be difficult for folks to get their heads around what go try(f)
or defer try(f)
were doing and that it's best therefore to just prohibit them altogether.
I agree with those who think that the existing error handling techniques would look less verbose if go fmt
didn't rewrite single line if
statements. Personally, I would prefer a simple rule that this would be allowed for _any_ single statement if
whether concerned with error handling or not. In fact I have never been able to understand why this isn't currently allowed when writing single-line functions where the body is placed on the same line as the declaration is allowed.
In the case decorating errors
func myfunc()( err error){
try(thing())
defer func(){
err = errors.Wrap(err,"more context")
}()
}
This feels considerably more verbose and painful than the existing paradigms, and not as concise as check/handle. The non-wrapping try() variant is more concise, but it feels like people will end up using a mix of try, and plain error returns. I'm not sure I like the idea of mixing try and simple error returns, but I'm totally sold on decorating errors (and looking forward to Is/As). Make me think that whilst this is syntactically neat, I'm not sure I would want to actually use it. check/handle felt something I would more thoroughly embrace.
I really like the simplicity of this and the "do one thing well" approach. In my GoAWK interpreter it would be very helpful -- I have about 100 if err != nil { return nil }
constructs that it would simplify and tidy up, and that's in a fairly small codebase.
I've read the proposal's justification for making it a builtin rather than a keyword, and it boils down to not having to adjust the parser. But isn't that a relatively small amount of pain for compiler and tooling writers, whereas having the extra parens and the this-looks-like-a-function-but-isn't readability issues will be something all Go coders and code-readers have to endure. In my opinion the argument (excuse? :-) that "but panic()
does control flow" doesn't cut it, because panic and recover are by their very nature, exceptional, whereas try()
will be normal error handling and control flow.
I'd definitely appreciate it even if this went in as is, but my strong preference would be for normal control flow to be clear, i.e., done via a keyword.
I'm in favour of this proposal. It avoids my largest reservation about the previous proposal: the non-orthogonality of handle
with respect to defer
.
I'd like to mention two aspects that I don't think have been highlighted above.
Firstly, although this proposal doesn't make it easy to add context-specific error text to an error, it _does_ make it easy to add stack frame error-tracing information to an error: https://play.golang.org/p/YL1MoqR08E6
Secondly, try
is arguably a fair solution to most of the problems underlying https://github.com/golang/go/issues/19642. To take an example from that issue, you could use try
to avoid writing out all the return values each time. This is also potentially useful when returning by-value struct types with long names.
func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
xx := int(x)
if f.NumGlyphs() <= xx {
try(ErrNotFound)
}
i := f.cached.locations[xx+0]
j := f.cached.locations[xx+1]
if j < i {
try(errInvalidGlyphDataLength)
}
if j-i > maxGlyphDataLength {
try(errUnsupportedGlyphDataLength)
}
buf, err = b.view(&f.src, int(i), int(j-i))
return buf, i, j - i, err
}
I like this proposal also.
And I have a request.
Like make
, can we allow try
to take a variable number of parameters
This way, it is one builtin that can handle all use-cases, while still being explicit. Its advantages:
While repetitiveif err !=nil { return ... err }
is certainly an ugly stutter, I'm with those
who think the try() proposal is very low on readability and somewhat inexplicit.
The use of named returns is problematic too.
If this sort of tidying is needed, why not try(err)
as syntactic sugar for
if err !=nil { return err }
:
file, err := os.Open("file.go")
try(err)
for
file, err := os.Open("file.go")
if err != nil {
return err
}
And if there is more than one return value, try(err)
could return t1, ... tn, err
where t1, ... tn are the zero values of the other return values.
This suggestion can obviate the need for named return values and be,
in my view, easier to understand and more readable.
Even better, I think would be:
file, try(err) := os.Open("file.go")
Or even
file, err? := os.Open("file.go")
This last is backwards compatible (? is currently not allowed in identifiers).
(This suggestion is related to https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes. But the recurring theme examples seem different because that was at a stage when an explicit handle was still being discussed instead of leaving that to a defer.)
Thanks to the go team for this careful, interesting proposal.
@rogpeppe comment if try
auto-adds the stack frame, not me, I'm ok with it discouraging adding context.
@aarzilli - So according to your proposal, is a defer clause mandatory every time we give extra parameters to tryf
?
What happens if I do
try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)
and do not write a defer function ?
@agnivade
What happens if I do (...) and do not write a defer function ?
typecheck error.
In my opinion, using try
to avoid writing out all the return values is actually just another strike against it.
func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
xx := int(x)
if f.NumGlyphs() <= xx {
try(ErrNotFound)
}
//...
I completely understand the desire to avoid having to write out return nil, 0, 0, ErrNotFound
, but I would much rather solve that some other way.
The word try
doesn't mean "return". And that's how it's being used here. I would actually prefer that the proposal change so that try
can't take an error
value directly, because I don't ever want anyone writing code like that ^^ . It reads wrong. If you showed that code to a newbie, they'd have no clue what that try was doing.
If we want a way to easily just return defaults and an error value, let's solve that separately. Maybe another builtin like
return default(ErrNotFound)
At least that reads with some kind of logic.
But let's not abuse try
to solve some other problem.
@natefinch if the try
builtin is named check
like in the original proposal, it would be check(err)
which reads considerably better, imo.
Putting that aside, I don't know if it's really an abuse to write try(err)
. It falls out of the definition cleanly. But, on the other hand, that also means that this is legal:
a, b := try(1, f(), err)
I guess my main problem with try
is that it's really just a panic
that only goes up one level... except that unlike panic, it's an expression, not a statement, so you can hide it in the middle of a statement somewhere. That almost makes it worse than panic.
@natefinch If you conceptualize it like a panic that goes up one level and then does other things, it seems pretty messy. However, I conceptualize it differently. Functions that return errors in Go are effectively returning a Resulttry
is a utility that unpacks the result and either returns an "error result" if error != nil
, or unpacks the T portion of the result if error == nil
.
Of course, in Go we don't actually have result objects, but it's effectively the same pattern and try
seems like a natural codification of that pattern. I believe that any solution to this problem is going to have to codify some aspect of error handling, and try
s take on it seems reasonable to me. Myself and others are suggesting to extend the capability of try
a bit to better fit existing Go error handling patterns, but the underlying concept remains the same.
@ugorji The try(f, bool)
variant you propose sounds like the must
from #32219.
@ugorji The
try(f, bool)
variant you propose sounds like themust
from #32219.
Yes, it is. I just felt like they all 3 cases could be handled with a singular builtin function, and satisfy all use-cases elegantly.
Since try()
is already magical, and aware of the error return value, could it be augmented to also return a pointer to that value in when called in the nullary (zero argument) form? That would eliminate the need for named returns, and I believe, help to visually correlate where the error is expected to come from in defer statements. For example:
func foo() error {
defer fmt.HandleErrorf(try(), "important foo context info")
try(bar())
try(baz())
try(etc())
}
@ugorji
I think the boolean on try(f, bool)
would make it hard to read and easy to miss. I like your proposal but for the panic case I think that that could be left out for users to write that inside the handler from your third bullet, e.g. try(f(), func(err error) { panic('at the disco'); })
, this makes it more explicit for users than a hidden try(f(), true)
that is easy to overlook, and I don't think the builtin functions should encourage panics.
@ugorji
I think the boolean ontry(f, bool)
would make it hard to read and easy to miss. I like your proposal but for the panic case I think that that could be left out for users to write that inside the handler from your third bullet, e.g.try(f(), func(err error) { panic('at the disco'); })
, this makes it more explicit for users than a hiddentry(f(), true)
that is easy to overlook, and I don't think the builtin functions should encourage panics.
On further thought, I tend to agree with your position and your reasoning, and it still looks elegant as a one-liner.
@patrick-nyt is still another proponent of _assignment syntax_ to trigger a nil test, in https://github.com/golang/go/issues/32437#issuecomment-499533464
This concept appears in 13 separate responses to the check/handle proposal
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes
f, ?return := os.Open(...)
f, ?panic := os.Open(...)
Why? Because it reads like Go 1, whereas try()
and check
do not.
One objection to try
seems to be that it is an expression. Suppose instead that there is a unary postfix statement ?
that means return if not nil. Here is the standard code sample (assuming that my proposed deferred package is added):
func CopyFile(src, dst string) error {
var err error // Don't need a named return because err is explicitly named
defer deferred.Annotate(&err, "copy %s %s", src, dst)
r, err := os.Open(src)
err?
defer deferred.AnnotatedExec(&err, r.Close)
w, err := os.Create(dst)
err?
defer deferred.AnnotatedExec(&err, r.Close)
defer deferred.Cond(&err, func(){ os.Remove(dst) })
_, err = io.Copy(w, r)
return err
}
The pgStore example:
func (p *pgStore) DoWork() error {
tx, err := p.handle.Begin()
err?
defer deferred.Cond(&err, func(){ tx.Rollback() })
var res int64
err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
// tricky bit: this would not change the value of err
// but the deferred.Cond would still be triggered by err being set before
deferred.Format(err, "insert table")?
_, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
deferred.Format(err, "insert table2")?
return tx.Commit()
}
I like this from @jargv:
Since try() is already magical, and aware of the error return value, could it be augmented to also return a pointer to that value in when called in the nullary (zero argument) form? That would eliminate the need for named returns
But instead of overloading the name try
based on the number of args, I think there could be another magic builtin, say reterr
or something.
I have briefed through some very often used packages, looking for go code that "suffers" from err handling but must have been well thought before written, trying to figure out what "magic" would the proposed try() would do.
Currently, unless I misunderstood the proposal, many of those (e.g. not super basic error handling) would not gain much, or would have to stay with the "old" error handling style.
Example from net/http/request.go:
func (r *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
`
trace := httptrace.ContextClientTrace(r.Context())
if trace != nil && trace.WroteRequest != nil {
defer func() {
trace.WroteRequest(httptrace.WroteRequestInfo{
Err: err,
})
}()
}
// Find the target host. Prefer the Host: header, but if that
// is not given, use the host from the request URL.
//
// Clean the host, in case it arrives with unexpected stuff in it.
host := cleanHost(r.Host)
if host == "" {
if r.URL == nil {
return errMissingHost
}
host = cleanHost(r.URL.Host)
}
// According to RFC 6874, an HTTP client, proxy, or other
// intermediary must remove any IPv6 zone identifier attached
// to an outgoing URI.
host = removeZone(host)
ruri := r.URL.RequestURI()
if usingProxy && r.URL.Scheme != "" && r.URL.Opaque == "" {
ruri = r.URL.Scheme + "://" + host + ruri
} else if r.Method == "CONNECT" && r.URL.Path == "" {
// CONNECT requests normally give just the host and port, not a full URL.
ruri = host
if r.URL.Opaque != "" {
ruri = r.URL.Opaque
}
}
if stringContainsCTLByte(ruri) {
return errors.New("net/http: can't write control character in Request.URL")
}
// TODO: validate r.Method too? At least it's less likely to
// come from an attacker (more likely to be a constant in
// code).
// Wrap the writer in a bufio Writer if it's not already buffered.
// Don't always call NewWriter, as that forces a bytes.Buffer
// and other small bufio Writers to have a minimum 4k buffer
// size.
var bw *bufio.Writer
if _, ok := w.(io.ByteWriter); !ok {
bw = bufio.NewWriter(w)
w = bw
}
_, err = fmt.Fprintf(w, "%s %s HTTP/1.1\r\n", valueOrDefault(r.Method, "GET"), ruri)
if err != nil {
return err
}
// Header lines
_, err = fmt.Fprintf(w, "Host: %s\r\n", host)
if err != nil {
return err
}
if trace != nil && trace.WroteHeaderField != nil {
trace.WroteHeaderField("Host", []string{host})
}
// Use the defaultUserAgent unless the Header contains one, which
// may be blank to not send the header.
userAgent := defaultUserAgent
if r.Header.has("User-Agent") {
userAgent = r.Header.Get("User-Agent")
}
if userAgent != "" {
_, err = fmt.Fprintf(w, "User-Agent: %s\r\n", userAgent)
if err != nil {
return err
}
if trace != nil && trace.WroteHeaderField != nil {
trace.WroteHeaderField("User-Agent", []string{userAgent})
}
}
// Process Body,ContentLength,Close,Trailer
tw, err := newTransferWriter(r)
if err != nil {
return err
}
err = tw.writeHeader(w, trace)
if err != nil {
return err
}
err = r.Header.writeSubset(w, reqWriteExcludeHeader, trace)
if err != nil {
return err
}
if extraHeaders != nil {
err = extraHeaders.write(w, trace)
if err != nil {
return err
}
}
_, err = io.WriteString(w, "\r\n")
if err != nil {
return err
}
if trace != nil && trace.WroteHeaders != nil {
trace.WroteHeaders()
}
// Flush and wait for 100-continue if expected.
if waitForContinue != nil {
if bw, ok := w.(*bufio.Writer); ok {
err = bw.Flush()
if err != nil {
return err
}
}
if trace != nil && trace.Wait100Continue != nil {
trace.Wait100Continue()
}
if !waitForContinue() {
r.closeBody()
return nil
}
}
if bw, ok := w.(*bufio.Writer); ok && tw.FlushHeaders {
if err := bw.Flush(); err != nil {
return err
}
}
// Write body and trailer
err = tw.writeBody(w)
if err != nil {
if tw.bodyReadError == err {
err = requestBodyReadError{err}
}
return err
}
if bw != nil {
return bw.Flush()
}
return nil
}
`
or as used in a thorough test such as pprof/profile/profile_test.go:
`
func checkAggregation(prof *Profile, a *aggTest) error {
// Check that the total number of samples for the rows was preserved.
total := int64(0)
samples := make(map[string]bool)
for _, sample := range prof.Sample {
tb := locationHash(sample)
samples[tb] = true
total += sample.Value[0]
}
if total != totalSamples {
return fmt.Errorf("sample total %d, want %d", total, totalSamples)
}
// Check the number of unique sample locations
if a.rows != len(samples) {
return fmt.Errorf("number of samples %d, want %d", len(samples), a.rows)
}
// Check that all mappings have the right detail flags.
for _, m := range prof.Mapping {
if m.HasFunctions != a.function {
return fmt.Errorf("unexpected mapping.HasFunctions %v, want %v", m.HasFunctions, a.function)
}
if m.HasFilenames != a.fileline {
return fmt.Errorf("unexpected mapping.HasFilenames %v, want %v", m.HasFilenames, a.fileline)
}
if m.HasLineNumbers != a.fileline {
return fmt.Errorf("unexpected mapping.HasLineNumbers %v, want %v", m.HasLineNumbers, a.fileline)
}
if m.HasInlineFrames != a.inlineFrame {
return fmt.Errorf("unexpected mapping.HasInlineFrames %v, want %v", m.HasInlineFrames, a.inlineFrame)
}
}
// Check that aggregation has removed finer resolution data.
for _, l := range prof.Location {
if !a.inlineFrame && len(l.Line) > 1 {
return fmt.Errorf("found %d lines on location %d, want 1", len(l.Line), l.ID)
}
for _, ln := range l.Line {
if !a.fileline && (ln.Function.Filename != "" || ln.Line != 0) {
return fmt.Errorf("found line %s:%d on location %d, want :0",
ln.Function.Filename, ln.Line, l.ID)
}
if !a.function && (ln.Function.Name != "") {
return fmt.Errorf(`found file %s location %d, want ""`,
ln.Function.Name, l.ID)
}
}
}
return nil
}
`
These are two examples I can think of in which one would say : "I would like a better error handling option"
Can someone demonstrate how would these improve using try() ?
I'm mostly in favor of this proposal.
My main concern, shared with many commenters, is about named result parameters. The current proposal certainly encourages much more use of named result parameters and I think that would be a mistake. I don't believe this is simply a matter of style as the proposal states: named results are a subtle feature of the language which, in many cases, makes the code more bug-prone or less clear. After ~8 years of reading and writing Go code, I really only use named result parameters for two purposes:
error
) inside a deferTo attack this issue from a new direction, here's an idea which I don't think closely aligns with anything that has been discussed in the design document or this issue comment thread. Let's call it "error-defers":
Allow defer to be used to call functions with an implicit error parameter.
So if you have a function
func f(err error, t1 T1, t2 T2, ..., tn Tn) error
Then, in a function g
where the last result parameter has type error
(i.e., any function where try
my be used), a call to f
may be deferred as follows:
func g() (R0, R0, ..., error) {
defer f(t0, t1, ..., tn) // err is implicit
}
The semantics of error-defer are:
f
is called with the last result parameter of g
as the first input parameter of f
f
is only called if that error is not nilf
is assigned to the last result parameter of g
So to use an example from the old error-handling design doc, using error-defer and try, we could do
func printSum(a, b string) error {
defer func(err error) error {
return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
}()
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x+y)
return nil
}
Here's how HandleErrorf would work:
func printSum(a, b string) error {
defer handleErrorf("printSum(%q + %q)", a, b)
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x+y)
return nil
}
func handleErrorf(err error, format string, args ...interface{}) error {
return fmt.Errorf(format+": %v", append(args, err)...)
}
One corner case that would need to be worked out is how to handle cases where it's ambiguous which form of defer we are using. I think that only happens with (very unusual) functions with signatures like this:
func(error, ...error) error
It seems reasonable to say that this case is handled in the non-error-defer way (and this preserves backward compatibility).
Thinking about this idea for the last couple of days, it is a little bit magical, but the avoidance of named result parameters is a large advantage in its favor. Since try
encourages more use of defer
for error manipulation, it makes some sense that defer
could be extended to better suit it to that purpose. Also, there's a certain symmetry between try
and error-defer.
Finally, error-defers are useful today even without try, since they supplant the use of named result parameters for manipulating error returns. For example, here's an edited version of some real code:
// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) (_ []*File, err error) {
files := make([]*file, len(keys))
defer func() {
if err != nil {
// Return any successfully retrieved files.
for _, f := range files {
if f != nil {
c.put(f)
}
}
}
}()
// ...
}
With error-defer, this becomes:
// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) ([]*File, error) {
files := make([]*file, len(keys))
defer func(err error) error {
// Return any successfully retrieved files.
for _, f := range files {
if f != nil {
c.put(f)
}
}
return err
}()
// ...
}
@beoran Regarding your comment that we should wait for generics. Generics won't help here - please read the FAQ.
Regarding your suggestions on @velovix 's 2-argument try
's default behavior: As I said before, your idea of what is the obviously reasonable choice is somebody else's nightmare.
May I suggest that we continue this discussion once a wide consensus evolves that try
with an explicit error handler is a better idea than the current minimal try
. At that point it makes sense to discuss the fine points of such a design.
(I do like having a handler, for that matter. It's one of our earlier proposals. And if we adopt try
as is, we still can move towards a try
with a handler in a forward-compatible way - at least if the handler is optional. But let's take one step at a time.)
@aarzilli Thanks for your suggestion.
As long as decorating errors is optional, people will lean towards not doing it (it's extra work after all). See also my comment here.
So, I don't think the proposed try
_discourages_ people from decorating errors (they are already discouraged even with the if
for the above reason); it's that try
doesn't _encourage_ it.
(One way of encouraging it is to tie it into try
: One can only use try
if one also decorates the error, or explicitly opts out.)
But back to your suggestions: I think you're introducing a whole lot more machinery here. Changing the semantics of defer
just to make it work better for try
is not something we would want to consider unless those defer
changes are beneficial in a more general way. Also, your suggestion ties defer
together with try
and thus makes both those mechanisms less orthogonal; something we would want to avoid.
But more importantly, I doubt you would want to force everybody to write a defer
just so they can use try
. But without doing that, we're back to square one: people will lean towards not decorating errors.
(I do like having a handler, for that matter. It's one of our earlier proposals. And if we adopt try as is, we still can move towards a try with a handler in a forward-compatible way - at least if the handler is optional. But let's take one step at a time.)
Sure, perhaps a multi-step approach is the way to go. If we add an optional handler argument in the future, tooling could be created to warn the writer of an unhandled try
in the same spirit as the errcheck
tool. Regardless, I appreciate your feedback!
@alanfo Thanks for your positive feedback.
Regarding the points you brought up:
1) If the only issue with try
is the fact that one will have to name an error return so we can decorate an error via defer
, I think we are good. If naming the result turns out to be a real issue, we could address it. A simple mechanism I can think of would be a predeclared variable that is an alias to an error result (think of it as holding the error which triggered the most recent try
). There may be better ideas. We didn't propose this because there is already a mechanism in the language, which is to name the result.
2) try
and testing: This can be addressed and made to work. See the detailed doc.
3) This is explicitly addressed in the detailed doc.
4) Acknowledged.
@benhoyt Thanks for your positive feedback.
If the main argument against this proposal is the fact that try
is a built-in, we're in a great spot. Using a built-in is simply a pragmatic solution for the backward-compatibility problem (it happens to cause no extra work for the parser, and tools etc. - but that's just a nice side benefit, not the primary reason). There are also some benefits to having to write parentheses, this is discussed in detail in the design doc (section on Properties of the proposed design).
All that said, if using a built-in is the showstopper, we should consider the try
keyword. It will not be backward-compatible though with existing code as the keyword may conflict with existing identifiers.
(To be complete, there's also the option of an operator such as ?
, which would be backward-compatible. It doesn't strike me as the best choice for a language such as Go, though. But again, if that's all it takes to make try
palatable, we should perhaps consider it.)
@ugorji Thanks for your positive feedback.
try
could be extended to take an additional argument. Our preference would be to take only a function with signature func (error) error
. If you want to panic, it's easy to provide a one-line helper function:
func doPanic(err error) error { panic(err) }
Better to keep the design of try
simple.
@patrick-nyt What you are suggesting:
file, err := os.Open("file.go")
try(err)
will be possible with the current proposal.
@dpinela , @ugorji Please read also the design doc on the subject of must
vs try
. It's better to keep try
as simple as possible. must
is a common "pattern" in initialization expressions, but there's no urgent need to "fix" that.
@jargv Thanks for your suggestion. This is an interesting idea (see also my comment here on this subject). To summarize:
try(x)
operates as proposedtry()
returns an *error
pointing to the error resultThis would be indeed another way to get to the result w/o having to name it.
@cespare The suggestion by @jargv looks much simpler to me than what you are proposing. It solves the same problem of access to the result error. What do you think?
As per https://github.com/golang/go/issues/32437#issuecomment-499320588:
func doPanic(err error) error { panic(err) }
I anticipate this function would be quite common. Could this be predefined in "builtin" (or somewhere else in a standard package eg errors
)?
Too bad that you do not anticipate generics powerful enough to implement
try, I actually would have hoped it would be possible to do so.
Yes, this proposal could be a first step, although I don't see much use in
it myself as it stands now.
Granted, this issue has perhaps too much focus on detailed alternatives,
but it goes to show that many participants are not completely happy with
it. What seems to be lacking is a wide consensus about this proposal...
Op vr 7 jun. 2019 01:04 schreef pj notifications@github.com:
Asper #32437 (comment)
https://github.com/golang/go/issues/32437#issuecomment-499320588:func doPanic(err error) error { panic(err) }
I anticipate this function would be quite common. Could this be predefined
in "builtin"?—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAARM6OOOLLYO5ZCE6VVL2TPZGJWRA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXEMYZY#issuecomment-499698791,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAARM6K5AOR2DES4QDTNLSTPZGJWRANCNFSM4HTGCZ7Q
.
@pjebs, I’ve written the equivalent function dozens of times. I usually call it “orDie” or “check”. It’s so simple, there’s no real need to make it part of the standard library. Plus different people may want logging or whatever before termination.
@beoran Perhaps you could expand on the connection between generics and error handling. When I think about them, they seem like two different things. Generics is not a catch all that can address all problems with the language. It is the ability to write a single function that can operate on multiple types.
This specific error handling proposal tries to reduce boilerplate by introducing a predeclared function try
that changes the flow control in some circumstances. Generics will never change the flow of control. So I really don't see the relationship.
My initial reaction to this was a 👎 as I imagined that handling several error prone calls within a function would make the defer
error handle confusing. After reading through the whole proposal, I have flipped my reaction to a ❤️ and 👍 as I learnt that this can still be achieved with relatively low complexity.
@carlmjohnson Yes it is simple but...
I’ve written the equivalent function dozens of times.
The advantages of a predeclared function is:
@griesemer With the error-handler variant of original try proposal, is the overarching function's requirement for returning error now no longer required.
When I first enquired about it the err => panic, I was pointed out that the proposal considered it but considered it too dangerous (for good reason). But if we make the use of try()
without a error-handler in a scenario where overarching function doesn't return error, making it into a compile-time error alleviates the concern discussed in the proposal
@pjebs The overarching function's requirement for returning an error was not required in the original design _if_ an error handler was provided. But it's just another complication of try
. It is _much_ better to keep it simple. Instead, it would be clearer to have a separate must
function, which always panics on error (but otherwise is like try
). Then it's obvious what happens in the code and one doesn't have to look at the context.
The main attraction of having such a must
would be that it could be used with unit tests; especially if the testing
package were suitably adjusted to recover from the panics caused by must
and report them as test failures in a nice way. But why add yet another new language mechanism when we can just adjust the testing package to also accept test function of the form TestXxx(t *testing.T) error
? If they return an error, which seems quite natural after all (perhaps we should have done this from the start), then try
will work just fine. Local tests will need a bit more work, but it's probably doable.
The other relatively common use for must
is in global initialization expressions (must(regexp.Compile...
, etc.). If would be a "nice to have" but that does not necessarily raise it to the level required for a new language feature.
@griesemer Given that must
is vaguely related to try
, and given that the momentum is towards try
getting implemented, don't you think it's good to consider must
at the same time - even if it's merely a "nice to have".
The chances are that if it's not discussed in this round, it simply won't get implemented/seriously considered, at least for 3+ years (or perhaps ever). The overlap in discussion would also be good rather than starting from scratch and recycling discussions.
Many people have stated that must
compliments try
very nicely.
@pjebs It certainly doesn't appear that there's any "momentum towards try
getting implemented" right now... - And we also only just posted this two days ago. Nor has anything been decided. Let's give this some time.
It has not escaped us that must
dovetails nicely with try
, but that's not the same as making it part of the language. We only have started to explore this space with a wider group of people. We really don't know yet what might come up in support or against it. Thanks.
After spending hours reading all the comments and the detailed design doc I wanted to add my views to this proposal.
I will do my best to respect @ianlancetaylor's request not to just restate previous points but to instead add new comments to the discussion. However I don't think I can make the new comments without somewhat referencing prior comments.
The preference to overload the obvious and straightforward nature of defer
as alarming. If I write defer closeFile(f)
that is straightforward and obvious to me what is happening and why; at the end of the func that will be called. And while using defer
for panic()
and recover()
is less obvious, I rarely if ever use it and almost never see it when reading other's code.
Spoo to overload defer
to also handle errors is not obvious and confusing. Why the keyword defer
? Does defer
not mean _"Do later"_ instead of _"Maybe to later?"_
Also there is the concern mentioned by the Go team about defer
performance. Given that, it seems doubly unfortunate that defer
is being considered for the _"hot path"_ code flow.
As @prologic mentioned, is this try()
proposal predicated on a large percentage of code that would use this use-case, or is it instead based on attempting to placate those who have complained about Go error handling?
I wish I knew how to give you stats from my code base without exhaustively reviewing every file and take notes; I don't know how @prologic was able to though glad he did.
But anecdotally I would be surprised if try()
addressed 5% of my use-cases and would suspect that it would address less than 1%. Do you know for certain that others have vastly different results? Have you taken a subset of the standard library and tried to see how it would be applied?
Because without known stats that this is appropriate to a large chuck of code in the wild I have to ask is this new complicating change to the language that will require everyone to learn the new concepts really address a compelling number of use-cases?
This is a total repeat of what others have comments, but what basically providing try()
is analogous in many ways to simply embracing the following as idomatic code, and this is code that will never find its way into any code any self-respecting developer ships:
f, _ := os.Open(filename)
I know I can be better in my own code, but I also know many of us depend on the largess of other Go developers who publish some tremendously useful packages, but from what I have seen in _"Other People's Code(tm)"_ best practices in error handling is often ignored.
So seriously, do we really want to make it easier for developers to ignore errors and allow them to polute GitHub with non-robust packages?
try()
in userlandUnless I misunderstand the proposal - which I probably do — here is try()
in the Go Playground implemented in userland, albeit with just one (1) return value and returning an interface instead of the expected type:
package main
import (
"errors"
"fmt"
"strings"
)
func main() {
defer func() {
r := recover()
if r != nil && strings.HasPrefix(r.(string),"TRY:") {
fmt.Printf("Ouch! %s",strings.TrimPrefix(r.(string),"TRY: "))
}
}()
n := try(badjuju()).(int)
fmt.Printf("Just chillin %dx!",n)
}
func badjuju() (int,error) {
return 10, errors.New("this is a really bad error")
}
func try(args ...interface{}) interface{} {
err,ok := args[1].(error)
if ok && err != nil {
panic(fmt.Sprintf("TRY: %s",err.Error()))
}
return args[0]
}
So the user could add a try2()
, try3()
and so on depending on how many return values they needed to return.
But Go would only need one (1) simple _yet universal_ language feature to allow users who want try()
to roll their own support, albeit one that still requires explicit type assertion. Add a _(fully backward-compatible)_ capability for a Go func
to return a variadic number of return values, e.g.:
func try(args ...interface{}) ...interface{} {
err,ok := args[1].(error)
if ok && err != nil {
panic(fmt.Sprintf("TRY: %s",err.Error()))
}
return args[0:len(args)-2]
}
And if you address generics first then the type assertions would not even be necessary _(although I think the use-cases for generics should be whittled down by adding builtins to address generic's use-cases rather than add the confusing semantics and syntax salad of generics from Java et. al.)_
When studying the proposal's code I find that the behaviour is non-obvious and somewhat hard to reason about.
When I see try()
wrapping an expression, what will happen if an error is returned?
Will the error just be ignored? Or will it jump to the first or the most recent defer
, and if so will it automatically set a variable named err
inside the closure that, or will it pass it as a parameter _(I don't see a parameter?)_. And if not an automatic error name, how do I name it? And does that mean I can't declare my own err
variable in my function, to avoid clashes?
And will it call all defer
s? In reverse order or regular order?
Or will it return from both the closure and the func
where the error was returned? _(Something I would never have considered if I had not read here words that imply that.)_
After reading the proposal and all the comments thus far I still honestly do not know the answers to the above questions. Is that the kind of feature we want to add to a language whose advocates champion as being _"Captain Obvious?"_
Using defer
, it appears the only control developers would be afforded is to branch to _(the most recent?)_ defer
. But in my experience with any methods beyond a trivial func
it is usually more complicated than that.
Often I have found it to be helpful to share aspect of error handling within a func
— or even across a package
— but then also have more specific handling shared across one or more other packages.
For example, I may call five (5) func
calls that return an error()
from within another func
; let's label them A()
, B()
, C()
, D()
, and E()
. I may need C()
to have its own error handling, A()
, B()
, D()
, and E()
to share some error handling, and B()
and E()
to have specific handling.
But I do not believe it would be possible to do that with this proposal. At least not easily.
Ironically, however, Go already has language features that allow a high level of flexibility that does not need to be limited to a small set of use-cases; func
s and closures. So my rhetorical question is:
_"Why can't we just add slight enhancements to the existing language to address these use-cases and not need to add new builtin functions or accept confusing semantics?"_
It is a rhetorical question because I plan to submit a proposal as an alternative, one that I conceived of during the study of this proposal and while considering all its drawbacks.
But I digress, that will come later and this comment is about why the current proposal needs to be reconsidered.
break
This may feel like it comes out of left field as most people use early returns for error handling, but I have found it is be preferable to use break
for error handling wrapping most or all of a func prior to return
.
I have used this approach for a while and its benefits in easing refactoring alone make it preferable to early return
, but it has several other benefits including single exit point and ability to terminate a section of a func early but still be able to run cleanup _(which is probably why I so rarely use defer
, which I find harder to reason about in terms of program flow.)_
To use break
instead of early return use a for range "1" {...}
loop to create a block for the break to exit from _(I actually create a package called only
that only contains a constant called Once
with a value of "1"
):_
func (me *Config) WriteFile() (err error) {
for range only.Once {
var j []byte
j, err = json.MarshalIndent(me, "", " ")
if err != nil {
err = fmt.Errorf("unable to marshal config; %s",
err.Error(),
)
break
}
err = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
if err != nil {
err = fmt.Errorf("unable to make directory'%s'; %s",
me.GetDir(),
err.Error(),
)
break
}
err = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
if err != nil {
err = fmt.Errorf("unable to write to config file '%s'; %s",
me.GetFilepath(),
err.Error(),
)
break
}
}
return err
}
I plan to blog about the pattern at length in the near future, and discuss the several reasons why I have found it to work better than early returns.
But I digress. My reason from bringing it up here is I would have for Go to implement error handling that assumes early return
s and ignores using break
for error handling
err == nil
is problematicAs further digression, I want to bring up the concern I have felt about idiomatic error handling in Go. While I am a huge believer of Go's philosophy to handle errors when they occur vs. using exception handling I feel the use of nil
to indicate no error is problematic because I often find I would like to return a success message from a routine — for use in API responses — and not only return a non-nil value just when there is an error.
So for Go 2 I would really like to see Go consider adding a new builtin type of status
and three builtin functions iserror()
, iswarning()
, issuccess()
. status
could implement error
— allowing for much backward compatibility and a nil
value passed to issuccess()
would return true
— but status
would have an additional internal state for error level so that testing for error level would always be done with one of the builtin functions and ideally never with a nil
check. That would allow something like the following approach instead:
func (me *Config) WriteFile() (sts status) {
for range only.Once {
var j []byte
j, sts = json.MarshalIndent(me, "", " ")
if iserror(sts) {
sts.AddMessage("unable to marshal config")
break
}
sts = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
if iserror(sts) {
sts.AddMessage("unable to make directory'%s'", me.GetDir())
break
}
sts = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
if iserror(sts) {
sts.AddMessage("unable to write to config file '%s'",
me.GetFilepath(),
)
break
}
sts = fmt.Status("config file written")
}
return sts
}
I am already using a userland approach in a pre-beta level currently internal-use package that is similar to the above for error handling. Frankly I spend a lot less time thinking about how to structure code when using this approach than when I was trying to follow idiomatic Go error handling.
If you think there is any chance of evolving idiomatic Go code to this approach, please take it into consideration when implementing error handling, including when considering this try()
proposal.
One of the key responses from the Go team has been _"Again, this proposal does not attempt to solve all error handling situations."_
And that is probably the most troubling concern, from a governance perspective.
Does this new complicating change to the language that will require everyone to learn the new concepts really address a compelling number of use-cases?
And is that not the same justification members of the core team have denied numerous feature requests from the community? The following is a direct quote from comment made by a member of the Go team in an archetypical response to a feature request submitted about 2 years ago _(I'm not naming the person or the specific feature request because this discussion should not be able the people but instead about the language):_
_"A new language feature needs compelling use cases. All language features are useful, or nobody would propose them; the question is: are they useful enough to justify complicating the language and requiring everyone to learn the new concepts? What are the compelling use cases here? How will people use these? For example, would people expect to be able to ... and if so how would they do that? Does this proposal do more than let you...?"_
— A core Go team member
Frankly when I have seen those responses I have felt one of two feelings:
But in either case my feelings were/are irrelevant; I understand and agree that part of the reason Go is the language so many of us choose to develop in is because of that jealous guarding of the purity of the language.
And that is why this proposal troubles me so, because the Go core team seems to be digging in on this proposal to the same level of someone who dogmatically wants an esoteric feature that there is no way in hell the Go community will ever tolerate.
_(And I truly hope the team will not shoot the messenger and take this as constructive criticism from someone who wants to see Go continue being the best it can be for all of us as I would have to be considered "Persona non grata" by the core team.)_
If requiring a compelling set of real-world use-cases is the bar for all community-generated feature proposals, should it not also be the same bar for _all_ feature proposals?
This too was covered by a few, but I want to draw comparison between try()
and the continued request for ternary operators. Quoting from another Go team member's comments about 18 months ago:
_"when "programming in the large" (large code bases with large teams over long periods of time), code is read WAY more often than it's written, so we optimize for readability, not writability."_
One of the _primary_ stated reasons for not adding ternary operators is that they are hard to read and/or easy to misread when nested. Yet the same can be true of nested try()
statements like try(try(try(to()).parse().this)).easily())
.
Additional reasons for argue against ternary operators have been that they are _"expressions"_ with that argument that nested expressions can add complexity. But does not try()
create a nestable expression too?
Now someone here said _"I think examples like [nested try()
s] are unrealistic"_ and that statement was not challenged.
But if people accept as postulate that developers won't nest try()
then why is the same deference not given to ternary operators when people say _"I think deeply nested ternary operators are unrealistic?"_
Bottom line for this point, I think if the argument against ternary operators are really valid, then they also should be considered valid arguments against this try()
proposal.
At the time of this writing the 58%
down votes to 42%
up votes. I think this alone should be enough to indicate that this is divisive enough of a proposal that it is time to return to the drawing board on this issue.
P.S. To put it more tongue-in-cheek, I think we should follow the paraphrased wisdom of Yoda:
_"There is no
try()
. Onlydo()
."_
@ianlancetaylor
@beoran Perhaps you could expand on the connection between generics and error handling.
Not speaking for @beoran but in my comment from a few minutes ago you'll see that if we had generics _(plus variadic return parameters)_ then we could build our own try()
.
However — and I will repeat what I said above about generics here where it will be easier to see:
_" I think the use-cases for generics should be whittled down by adding builtins to address generic's use-cases rather than add the confusing semantics and syntax salad of generics from Java et. al.)"_
@ianlancetaylor
When trying to formulate an answer to your question, I tried to implement the try
function in Go as it is, and to my delight, it's actually already possible to emulate something quite similar:
func try(v interface{}, err error) interface{} {
if err != nil {
panic(err)
}
return v
}
See here how it can be used: https://play.golang.org/p/Kq9Q0hZHlXL
The downsides to this approach are:
try
as in this proposal, a deferred handler is also needed if we want to do proper error handling. So I feel this is not a serious downside. It could even be better if Go had some kind of super(arg1, ..., argn)
builtin causes the caller of the caller, one level up the call stack, to return with the given arguments arg1,...argn, a sort of super return if you will. try
I implemented can only work with a function that returns a single result and an error. Sufficiently powerful generics could resolve problem 2 and 3, leaving only 1, which could be resolved by adding a super()
. With those two features in place, we could get something like:
func (T ... interface{})try(T, err error) super {
if err != nil {
super(err)
}
super(T...)
}
And then the deferred rescue would not be needed anymore. This benefit would be available even if no generics are added to Go.
Actually, this idea of a super() builtin is so powerful and interesting I might post a proposal for it separately.
@beoran Good to see we came to exactly the same constraints independently regarding implementing try()
in userland, except for the super part which I did not include because I wanted to talk about something similar in an alternate proposal. :-)
I like the proposal but the fact you had to explicitly specify that defer try(...)
and go try(...)
are disallowed made me think something was not quite right.... Orthogonality is a good design guide. On further reading and seeing things like
x = try(foo(...))
y = try(bar(...))
I wonder if may be try
needs to be a context! Consider:
try (
x = foo(...)
y = bar(...)
)
Here foo()
and bar()
return two values, the second of which is error
. Try semantics only matter for calls within the try
block where the returned error value is elided (no receiver) as opposed ignored (receiver is _
). You can even handle some errors in between foo
and bar
calls.
Summary:
a) the problem of disallowing try
for go
and defer
disappears by virtue of the syntax.
b) error handling of multiple functions can be factored out.
c) its magic nature is better expressed as special syntax than as a function call.
If try is a context then we've just made try/catch blocks which we're specifically trying to avoid (and for good reason)
There is no catch. Exact same code would be generated as when the current proposal has
x = try(foo(...))
y = try(bar(...))
This is just different syntax, not semantics.
````
I guess I had made a few assumptions about it that I shouldn't have done, although there's still a couple drawbacks to it.
What if foo or bar do not return an error, can they be placed in the try context as well? If not, that seems like it'd be kinda ugly to switch between error and non-error functions, and if they can, then we fall back to the issues of try blocks in older languages.
The second thing is that ypically the keyword ( ... )
syntax means you prefix the keyword on each line. So for import, var, const, etc: each line starts with the keyword. Making an exception to that rule doesn't seem like a good decision
Instead of using a function, would just be more idiomatic using a special identifier?
We already have the blank identifier _
which ignores values.
We could have something like #
which can only be used in functions which have the last returned value of type error.
func foo() (error) {
f, # := os.Open()
defer f.Close()
_, # = f.WriteString("foo")
return nil
}
when a error is assigned to #
the function returns immediately with the error received. As for the other variables their values would be:
@deanveloper, the try
block semantics only matter for functions that return an error value and where the error value is not assigned. So the last example of present proposal could also be written as
try(x = foo(...))
try(y = bar(...))
putting both statements within the same block is simiar to what we do for repeatedimport
, const
and var
statements.
Now if you have, e.g.
try(
x = foo(...))
go zee(...)
defer fum()
y = bar(...)
)
This is equivalent to writing
try(x = foo(...))
go zee(...)
defer fum()
try(y = bar(...))
Factoring out all of that in one try block makes it less busy.
Consider
try(x = foo())
If foo() doesn't return an error value, this is equivalent to
x = foo()
Consider
try(f, _ := os.open(filename))
Since returned error value is ignored, this is equivalent to just
f, _ := os.open(filename)
Consider
try(f, err := os.open(filename))
Since returned error value is not ignored, this is equivalent to
f, err := os.open(filename)
if err != nil {
return ..., err
}
As currently specified in the proposal.
And it also nicely declutters nested trys!
Here is a link to the alternate proposal I mentioned above:
It calls for adding two (2) small but general purpose language features to address the same use cases as try()
func
/closure in an assignment statement.break
, continue
or return
more than one level.With this two features their would be no _"magic"_ and I believe their usage would produce Go code that is easier to understand and more inline with the idiomatic Go code we are all familiar with.
I've read the proposal and really like where try is going.
Given how prevalent try is going to be, I wonder if making it a more default behavior would make it easier to handle.
Consider maps. This is valid:
v := m[key]
as is this:
v, ok := m[key]
What if we handle errors exactly the way try suggests, but remove the builtin. So if we started with:
v, err := fn()
Instead of writing:
v := try(fn())
We could instead write:
v := fn()
When the err value is not captured, it gets handled exactly as the try does. Would take a little getting used to, but it feels very similar to v, ok := m[key]
and v, ok := x.(string)
. Basically, any unhandled error causes the function to return and the err value to be set.
To go back to the design docs conclusions and implementation requirements:
• The language syntax is retained and no new keywords are introduced
• It continues to be syntactic sugar like try and hopefully is easy to explain.
• Does not require new syntax
• It should be completely backward compatible.
I imagine this would have nearly the same implementation requirements as try as the primary difference is rather than the builtin triggering the syntactic sugar, now it's the absence of the err field.
So using the CopyFile
example from the proposal along with defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
, we get:
func CopyFile(src, dst string) (err error) {
defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
r := os.Open(src)
defer r.Close()
w := os.Create(dst)
defer func() {
err := w.Close()
if err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
io.Copy(w, r)
w.Close()
return nil
}
@savaki I like thi and was thinking about what it would take to make Go flip the error handling by always handling errors by default and let the programmer specify when not to do so (by capturing the err into a variable) but total lack of any identifier would make code hard to follow as one wouldn't be able to see all the return points. May be a convention to name functions that could return an error differently could work (like capitalizing public identifiers). May be if a function returned an error, it must always end with, let's say ?
. Then Go could always implicitly handle the error and automatically return it to the calling function just like try. This makes it very similar to some proposals suggesting to use a ?
identifier instead of try but an important difference is that here ?
would be part of the name of the function and not an additional identifier. In fact a function that returned error
as the last return value wouldn't even compile if not suffixed ?
. Of course ?
is arbitrary and could be replaced with anything else that made the intent more explicit. operation?()
would be equivalent to wrapping try(someFunc())
but ?
would be part of the function name and it's sole purpose would be to indicate that the function can return an error just like the capitalizing the first letter of a variable.
This superficially ends up being very similar to other proposals asking to replace try
with ?
but a critical difference is that makes error handling implicit (automatic) and instead makes ignoring (or wrapping) errors explicit which kind of a best practice anyway. The most obvious problem with this of course is that it is not backward compatible and I'm sure there are many more.
That said, I'd be very interested in seeing how Go can make error handling the default/implicit case by automating it and let the programmer write a bit of extra code to ignore/override the handling. The challenge I think is how to make all the return points obvious in this case because without it errors will become more like exceptions in the sense that they could come from anywhere as the flow of the program would not make it obvious. One could say making errors implicit with visual indicator is the same as implementing try
and making errcheck
a compiler failure.
could we do something like c++ exceptions with decorators for old functions?
func some_old_test() (int, error){
return 0, errors.New("err1")
}
func some_new_test() (int){
if true {
return 1
}
throw errors.New("err2")
}
func throw_res(int, e error) int {
if e != nil {
throw e
}
return int
}
func main() {
fmt.Println("Hello, playground")
try{
i := throw_res(some_old_test())
fmt.Println("i=", i + some_new_test())
} catch(err io.Error) {
return err
} catch(err error) {
fmt.Println("unknown err", err)
}
}
@owais I was thinking the semantics would be exactly the same as try so at the very least you would need to declare the err type. So if we started with:
func foo() error {
_, err := fn()
if err != nil {
return err
}
return nil
}
If I understand the try proposal, simply doing this:
func foo() error {
_ := fn()
return nil
}
would not compile. One nice perk is that it gives the compile an opportunity to tell the user what's missing. Something to the effect that using implicit error handling requires the error return type to be named, err.
This then, would work:
func foo() (err error) {
_ := fn()
return nil
}
why not just handle the case of an error that isn't assigned to a variable.
implicit return for the if err != nil case, compiler can generate local variable name for returns if necessary can't be accessed by the programmer.
personally I dislike this particular case from a code readability standapoint
f := os.Open("foo.txt")
prefer an explicit return, follows the code is read more than written mantra
f := os.Open("foo.txt") else return
interestingly we could accept both forms, and have gofmt automatically add the else return.
adding context, also local naming of the variable. return becomes explicit because we want to add context.
f := os.Open("foo.txt") else err {
return errors.Wrap(err, "some context")
}
adding context with multiple return values
f := os.Open("foo.txt") else err {
return i, j, errors.Wrap(err, "some context")
}
nested functions require that the outer functions handle all results in the same order
minus the final error.
bits := ioutil.ReadAll(os.Open("foo")) else err {
// either error ends up here.
return i, j, errors.Wrap(err, "some context")
}
compiler refuses compilation due to missing error return value in function
func foo(s string) int {
i := strconv.Atoi(s) // cannot implicitly return error due to missing error return value for foo.
return i * 2
}
happily compiles because error is explicitly ignored.
func foo(s string) int {
i, _ := strconv.Atoi(s)
return i * 2
}
compiler is happy. it ignores the error as it currently does because no assignment or else suffix occurs.
func foo() error {
return errors.New("whoops")
}
func bar() {
foo()
}
within a loop you can use continue.
for _, s := range []string{"1","2","3","4","5","6"} {
i := strconv.Atoi(s) else continue
}
edit: replaced ;
with else
@savaki I think I understood your original comment and I like the idea of Go handling errors by default but I don't think it's viable without adding some additional syntax changes and once we do that, it becomes strikingly similar to the current proposal.
The biggest downside to what you propose is that it doesn't expose all the points from where a function can return unlike current if err != nil {return err}
or the try function introduced in this proposal. Even though it would function exactly the same way under the hood, visually the code would look very different. When reading code, there would be no way of knowing which function calls might return an error. That would end up being a worse experience than exceptions IMO.
May be error handling could be made implicit if the compiler forced some semantic convention on functions that could return errors. Like they must start or end with a certain phrase or character. That'd make all return points very obvious and I think it'd be better than manual error handling but not sure how significantly better considering there are already lint checks that cry out load when they spot an error being ignored. It'd be very interesting to see if the compiler can force functions to be named a certain way depending on whether they could return possible errors.
The main drawback of this approach is that the error result parameter needs to be named, possibly leading to less pretty APIs (but see the FAQs on this subject). We believe that we will get used to it once this style has established itself.
Not sure if something like this have been suggested before, can't find it here or in the proposal. Have you considered another builtin function that returns a pointer to the current function's error return value?
eg:
func example() error {
var err *error = funcerror() // always return a non-nil pointer
fmt.Print(*err) // always nil if the return parameters are not named and not in a defer
defer func() {
err := funcerror()
fmt.Print(*err) // "x"
}
return errors.New("x")
}
func exampleNamed() (err error) {
funcErr := funcerror()
fmt.Print(*funcErr) // "nil"
err = errors.New("x")
fmt.Print(*funcErr) // "x", named return parameter is reflected even before return is called
*funcErr = errors.New("y")
fmt.Print(err) // "y", unfortunate side effect?
defer func() {
funcErr := funcerror()
fmt.Print(*funcErr) // "z"
fmt.Print(err) // "z"
}
return errors.New("z")
}
usage with try:
func CopyFile(src, dst string) (error) {
defer func() {
err := funcerror()
if *err != nil {
*err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}()
// one liner alternative
// defer fmt.HandleErrorf(funcerror(), "copy %s %s", src, dst)
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
defer func() {
w.Close()
err := funcerror()
if *err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
try(io.Copy(w, r))
try(w.Close())
return nil
}
Alternatively funcerror (the name is a work in progress :D ) could return nil if not called inside defer.
Another alternative is that funcerror returns an "Errorer" interface to make it read-only:
type interface Errorer() {
Error() error
}
@savaki I actually do like your proposal to omit try()
and allow it to be more like testing a map or a type assertion. That feels much more _"Go-like."_
However, there is still one glaring issue I see, and that is your proposal presumes that all errors using this approach will trigger a return
and leave the function. What it does not contemplate is issuing a break
out of the current for
or a continue
for the current for
.
Early return
s are a sledgehammer when many times a scalpel is the better choice.
So I assert break
and continue
should be allowed to be valid error handling strategies and currently your proposal presumes only return
whereas try()
presumes that or calling an error handler that itself can only return
, not break
or continue
.
Looks like savaki and i had similar ideas, i just added the block semantics for dealing with the error if desired. For example adding comtext, for loops where you want to short circuit etc
@mikeschinkel see my extension, he and i had similar ideas i just extended it with an optional block statement
@james-lawrence
@mikesckinkel see my extension, he and i had similar ideas i just extended it with an optional block statement
Taking your example:
f := os.Open("foo.txt"); err {
return errors.Wrap(err, "some context")
}
Which compares to what we do today:
f,err := os.Open("foo.txt");
if err != nil {
return errors.Wrap(err, "some context")
}
Is definitely preferable to me. Except it has a few issues:
err
appears to be _"magically"_ declared. Magic should be minimized, no? So let's declare it:f, err := os.Open("foo.txt"); err {
return errors.Wrap(err, "some context")
}
nil
values as false
nor pointer values as true
, so it would need to be:f, err := os.Open("foo.txt"); err != nil {
return errors.Wrap(err, "some context")
}
And what that works, it starts to feel like just as much work and a lot of syntax on one line, so and I might continue to do the old way for clarity.
But what if Go added two (2) builtins; iserror()
and error()
? Then we could do this, which does not feel as bad to me:
f := os.Open("foo.txt"); iserror() {
return errors.Wrap(error(), "some context")
}
Or better _(something like):_
f := os.Open("foo.txt"); iserror() {
return error().Extend("some context")
}
What do you, and others think?
As an aside, check my username spelling. I would not have been notified of your mention if I wasn't paying attention anyway...
@mikeschinkel sorry about the name I was on my phone and github wasn't autosuggesting.
err appears to be "magically" declared. Magic should be minimized, no? So let's declare it:
meh, the entire idea of automatically inserting a return is magical. this is hardly the most magical thing going on in this entire proposal. Plus I'd argue the err was declared; just at the end inside a scoped block's context preventing it from polluting the parent scope while still maintaining all the good stuff we get normally with using if statements.
I'm generally pretty happy withs go's error handling with the upcoming additions to the errors package. I don't see anything in this proposal as super helpful. I'm just attempting to offer the most natural fit for the golang if we're dead set on doing it.
_"the entire idea of automatically inserting a return is magical."_
You won't get any argument from me there.
_"this is hardly the most magical thing going on in this entire proposal."_
I guess I was trying to argue that _"all magic is problematic."_
_"Plus I'd argue the err was declared; just at the end inside a scoped block's context..."_
So if I wanted to call it err2
this would work too?
f := os.Open("foo.txt"); err2 {
return errors.Wrap(err, "some context")
}
So I assume you are also proposing special case handling of the err
/err2
after the semi-colon, i.e. that it would be assumed to be either nil
or not nil
instead of bool
like when checking a map?
if _,ok := m[a]; !ok {
print("there is no 'a' in 'm'")
}
I'm generally pretty happy withs go's error handling with the upcoming additions to the errors package.
I too am happy with error handling, when combined with break
and continue
_(but not return
.)_
As it is, I see this try()
proposal as more harmful than helpful, and would rather see nothing than this implement as-proposed. #jmtcw.
@beoran @mikeschinkel Earlier I suggested that we could not implement this version of try
using generics, because it changes the control flow. If I'm reading correctly, you are both suggesting that we could use generics to implement try
by having it call panic
. But this version of try
very explicitly does not panic
. So we can't use generics to implement this version of try
.
Yes, we could use generics (a version of generics significantly more powerful than the one in the design draft at https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md) to write a function that panics on error. But panicking on error is not the kind of error handling that Go programmers write today, and it doesn't seem like a good idea to me.
@mikeschinkel special handling would be that the block executes only when there is an error.
```
f := os.Open('foo'); err { return err } // err would always be non-nil here.
@ianlancetaylor
_"Yes, we could use generics ... But panicking on error is not the kind of error handling that Go programmers write today, and it doesn't seem like a good idea to me."_
I actually strongly agree with you on this thus it appears you may have misinterpreted the intent of my comment. I was not at all suggesting that the Go team would implement error handling that used panic()
— of course not.
Instead I was trying to actually follow your lead from many of your past comments on other issues and suggested that we avoid making any changes to Go that are not absolutely necessary because they are instead possible in userland. So _if_ generics were addressed _then_ people who would want try()
could in fact implement it themselves, albeit by leveraging panic()
. And that would be one less feature that the team would need to add to and document for Go.
What I was not doing — and maybe that was not clear — was advocating that people actually use panic()
to implement try()
, just that they could if they really wanted to, and they had the features of generics.
Does that clarify?
To me calling panic
, however that is done, is quite different from this proposal for try
. So while I think I understand what you are saying, I do not agree that they are equivalent. Even if we had generics powerful enough to implement a version of try
that panics, I think there would still be a reasonable desire for the version of try
presented in this proposal.
@ianlancetaylor Acknowledged. Again, I was looking for a reason that try()
would not need to be added rather than find a way to add it. As I said above, I would far rather not have anything new for error handling than have try()
as proposed here.
I personally liked the earlier check
proposal more than this, based on purely visual aspects; check
had the same power as this try()
but bar(check foo())
is more readable to me than bar(try(foo()))
(I just needed a second to count the parens!).
More importantly, my main gripe about handle
/check
was that it didn't allow wrapping individual checks in different ways -- and now this try()
proposal has the same flaw, while invoking tricky rarely-used newbie-confusing features of defers and named returns. And with handle
at least we had the option of using scopes to define handle blocks, with defer
even that isn't possible.
As far as I'm concerned, this proposal loses to the earlier handle
/check
proposal in every single regard.
Here's another concern with using defers for error handling.
try
is a controlled/intended exit from a function. defers run always, including uncontrolled/unintended exits from functions. That mismatch could cause confusion. Here's an imaginary scenario:
func someHTTPHandlerGuts() (err error) {
defer func() {
recordMetric("db call failed")
return fmt.HandleErrorf("db unavailable: %v", err)
}()
data := try(makeDBCall)
// some code that panics due to a bug
return nil
}
Recall that net/http recovers from panics, and imagine debugging a production issue around the panic. You'd look at your instrumentation and see a spike in db call failures, from the recordMetric
calls. This might mask the true issue, which is the panic in the subsequent line.
I'm not sure how serious a concern this is in practice, but it is (sadly) perhaps another reason to think that defer is not an ideal mechanism for error handling.
Here's a modification that may help with some of the concerns raised: Treat try
like a goto
instead of like a return
. Hear me out. :)
try
would instead be syntactic sugar for:
t1, … tn, te := f() // t1, … tn, te are local (invisible) temporaries
if te != nil {
err = te // assign te to the error result parameter
goto error // goto "error" label
}
x1, … xn = t1, … tn // assignment only if there was no error
Benefits:
defer
is not required to decorate errors. (Named returns are still required, though.)error:
label is a visual clue that there is a try
somewhere in the function.This also provides a mechanism for adding handlers that sidesteps the handler-as-function problems: Use labels as handlers. try(fn(), wrap)
would goto wrap
instead of goto error
. The compiler can confirm that wrap:
is present in the function. Note that having handlers also helps with debugging: You can add/alter the handler to provide a debugging path.
Sample code:
func CopyFile(src, dst string) (err error) {
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
try(io.Copy(w, r), copyfail)
try(w.Close())
return nil
error:
return fmt.Errorf("copy %s %s: %v", src, dst, err)
copyfail:
recordMetric("copy failure") // count incidents of this failure
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
Other comments:
try
be preceded by a terminating statement. In practice, this would force them to the end of the function and could prevent some spaghetti code. On the other hand, it might prevent some reasonable, helpful uses.try
could be used to create a loop. I think this falls under the banner of "if it hurts, don't do it", but I'm not sure.Credit: I believe a variant of this idea was first suggested by @griesemer in person at GopherCon last year.
@josharian Thinking about the interaction with panic
is important here, and I'm glad you brought it up, but your example seems strange to me. In the following code it doesn't make sense to me that the defer always records a "db call failed"
metric. It would be a false metric if someHTTPHandlerGuts
succeeds and returns nil
. The defer
runs in all exit cases, not just error or panic cases, so the code seems wrong even if there is no panic.
func someHTTPHandlerGuts() (err error) {
defer func() {
recordMetric("db call failed")
return fmt.HandleErrorf("db unavailable: %v", err)
}()
data := try(makeDBCall)
// some code that panics due to a bug
return nil
}
@josharian Yes, this is more or less exactly the version we discussed last year (except that we used check
instead of try
). I think it would be crucial that one couldn't jump "back" into the rest of the function body, once we are at the error
label. That would ensure that the goto
is somewhat "structured" (no spaghetti code possible). One concern that was brought up was that the error handler (the error:
) label would always end up at the end of the function (otherwise one would have to jump around it somehow). Personally, I like the error handling code out of the way (at the end), but others felt that it should be visible right at the start.
@mikeshenkel I see returning from a loop as a plus rather than a negative. My guess is that would encourage developers to either use a separate function to handle the contents of a loop or explicitly use err as we currently do. Both of these seem like good outcomes to me.
From my POV, I don’t feel like this try syntax has to handle every use case just like I don’t feel that I need to use the
V, ok:= m[key]
Form from reading from a map
You could avoid the goto labels forcing handlers to the end of the function by resurrecting the handle
/check
proposal in a simplified form. What if we used the handle err { ... }
syntax but just didn't let handlers chain, instead only last one is used. It simplifies that proposal a lot, and is very similar with the goto idea, except it puts the handling closer to point of use.
func CopyFile(src, dst string) (err error) {
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // only if a “check” fails
}
}()
{
// handlers are scoped, after this scope the original handle is used again.
// as an alternative, we could have repeated the first handle after the io.Copy,
// or come up with a syntax to name the handlers, though that's often not useful.
handle err {
recordMetric("copy failure") // count incidents of this failure
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
check io.Copy(w, r)
}
check w.Close()
return nil
}
As a bonus, this has a future path to letting handlers chain, as all existing uses would have a return.
@josharian @griesemer if you introduce named handlers (which many responses to check/handle requested, see recurring themes), there are syntax options preferable to try(f(), err)
:
try.err f()
try?err f()
try#err f()
?err f() // because 'try' is redundant
?return f() // no handler
?panic f() // no handler
(?err f()).method()
f?err() // lead with function name, instead of error handling
f?err().method()
file, ?err := os.Open(...) // many check/handle responses also requested this style
One of the things I like most about Go is that its syntax is relatively punctuation-free, and can be read out loud without major problems. I would really hate for Go to end up as a $#@!perl
.
To me making "try" a built-in function and enabling chains has 2 problems:
I would prefer making it a statement without parenthesis. The examples in the proposal would require multiple lines but would become more readable (i.e, individual "try" instances would be harder to miss). Yes, it would break external parsers but I prefer to preserve consistency.
The ternary operator is another place were go does not have something and requires more keystrokes but at the same time improves readability/maintainability. Adding "try" in this more restricted form will better balance expressiveness vs readability, IMO.
FWIW, panic
affects control flow and has parens, but go
and defer
also affect flow and don’t. I tend to think that try
is more similar to defer
in that it’s an unusual flow operation and making it harder to do try (try os.Open(file)).Read(buf)
is good because we want to discourage one-liners anyway, but whatever. Either is fine.
Suggestion everyone will hate for an implicit name for a final error return variable: $err
. It’s better than try()
IMO. :-)
@griesemer
_"Personally, I like the error handling code out of the way (at the end)"_
+1 to that!
I find that error handling implemented _before_ the error occurs is much harder to reason about than error handling implemented _after_ the error occurs. Having to mentally jump back and force to follow the logic flow feels like I am back in 1980 writing Basic with GOTOs.
Let me propose yet another potential way to handle errors using CopyFile()
as the example again:
func CopyFile(src, dst string) (err error) {
r := os.Open(src)
defer r.Close()
w := os.Create(dst)
defer w.Close()
io.Copy(w, r)
w.Close()
for err := error {
switch err.Source() {
case w.Close:
os.Remove(dst) // only if a “try” fails
fallthrough
case os.Open, os.Create, io.Copy:
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
default:
err = fmt.Errorf("an unexpected error occurred")
}
}
return err
}
The language changes required would be:
Allow a for error{}
construct, similar to for range{}
but only entered upon an error and only executed once.
Allow omitting the capturing of return values that implement <object>.Error() string
but only when a for error{}
construct exists within the same func
.
Cause program control flow to jump to the first line of the for error{}
construct when an func
returns an _"error"_ in its last return value.
When returning an _"error"_ Go would add assign a reference to the func that returned the error which should be retrievable by <error>.Source()
Currently an _"error"_ is defined as any object that implements Error() string
and of course is not nil
.
However, there is often a need to extend error _even on success_ to allow returning values needed for success results of a RESTful API. So I would ask that the Go team please not automatically assume err!=nil
means _"error"_ but instead check if an error object implements an IsError()
and if IsError()
returns true
before assuming that any non-nil
value is an _"error."_
_(I am not necessarily talking about code in the standard library but primarily if you choose your control flow to branch on an _"error"_. If you only look at err!=nil
we will be very limited in what we can do in terms of return values in our functions.)_
BTW, allowing everyone to test for an _"error"_ the same way could probably most easily be done by adding a new builtin iserror()
function:
type ErrorIser interface {
IsError() bool
}
func iserror(err error) bool {
if err == nil {
return false
}
if _,ok := err.(ErrorIser); !ok {
return true
}
return err.IsError()
}
Note that allowing the non-capturing of the last _"error"_ from func
calls would allow later refactoring to return errors from func
s that initially did not need to return errors. And it would allow this refactoring without breaking any existing code that uses this form of error recovery and calls said func
s.
To me, that decision of _"Should I return an error or forgo error handling for calling simplicity?"_ is one of my biggest quandaries when writing Go code. Allowing non-capturing of _"errors"_ above would all but eliminate that quandary.
I actually tried to implement this idea as Go translator about half a year ago. I don't have strong opinion whether this feature should be added as Go builtin, but let me share the experience (though I'm not sure it's useful).
https://github.com/rhysd/trygo
I called the extended language TryGo and implemented TryGo to Go translator.
With the translator, the code
func CreateFileInSubdir(subdir, filename string, content []byte) error {
cwd := try(os.Getwd())
try(os.Mkdir(filepath.Join(cwd, subdir)))
p := filepath.Join(cwd, subdir, filename)
f := try(os.Create(p))
defer f.Close()
try(f.Write(content))
fmt.Println("Created:", p)
return nil
}
can be translated into
func CreateFileInSubdir(subdir, filename string, content []byte) error {
cwd, _err0 := os.Getwd()
if _err0 != nil {
return _err0
}
if _err1 := os.Mkdir(filepath.Join(cwd, subdir)); _err1 != nil {
return _err1
}
p := filepath.Join(cwd, subdir, filename)
f, _err2 := os.Create(p)
if _err2 != nil {
return _err2
}
defer f.Close()
if _, _err3 := f.Write(content); _err3 != nil {
return _err3
}
fmt.Println("Created:", p)
return nil
}
For the restriction of language, I could not implement generic try()
call. It is restricted to
but I could try this with my small project. My experience was
err
since its function's return value is determined by both assignment and try()
special function. very confusingtry()
function lacked 'wrapping error' feature as discussed above._"Both of these seem like good outcomes to me."_
We will have to agree to disagree here.
_"this try syntax (does not have to) handle every use case"_
That meme is probably the most troubling. At least given how resistant the Go team/community has been to any changes in the past that are not broadly applicable.
If we allow that justification here, why can't we revisit past proposals that have been turned down because they were not broadly applicable?
And are we now open to argue for changes in Go that are just useful for selected edge cases?
In my guess, setting this precedent will not produce good outcomes long term...
_"@mikeshenkel"_
P.S. I did not see your message at first because of misspelling. _(this does not offend me, I just don't get notified when my username is misspelled...)_
I appreciate the commitment to backwards compatibility that motivates you to make try
a builtin, rather than a keyword, but after wrestling with the utter _weirdness_ of having a frequently-used function that can change control flow (panic
and recover
are extremely rare), I got to wondering: has anyone done any large-scale analysis of the frequency of try
as an identifier in open source codebases? I was curious and skeptical, so I did a preliminary search across the following:
Across the 11,108,770 significant lines of Go living in these repositories, there were only 63 instances of try
being used as an identifier. Of course, I realize that these codebases (while large, widely used, and important in their own right) represent only a fraction of the Go code out there, and additionally, that we have no way to directly analyze private codebases, but it's certainly an interesting result.
Moreover, because try
, like any keyword, is lowercase, you'll never find it in a package's public API. Keyword additions will only affect package internals.
This is all preface to a few ideas I wanted to throw into the mix which would benefit from try
as a keyword.
I'd propose the following constructions.
1) No handler
// The existing proposal, but as a keyword rather than builtin. When an error is
// "caught", the function returns all zero values plus the error. Nothing
// particularly new here.
func doSomething() (int, error) {
try SomeFunc()
a, b := try AnotherFunc()
// ...
return 123, nil
}
2) Handler
Note that error handlers are simple code blocks, intended to be inlined, rather than functions. More on this below.
func doSomething() (int, error) {
// Inline error handler
a, b := try SomeFunc() else err {
return 0, errors.Wrap(err, "error in doSomething:")
}
// Named error handlers
handler logAndContinue err {
log.Errorf("non-critical error: %v", err)
}
handler annotateAndReturn err {
return 0, errors.Wrap(err, "error in doSomething:")
}
c, d := try SomeFunc() else logAndContinue
e, f := try OtherFunc() else annotateAndReturn
// ...
return 123, nil
}
Proposed restrictions:
try
a function call. No try err
.try
from within a function that returns an error as its rightmost return value. There's no change in how try
behaves based on its context. It never panics (as discussed much earlier in the thread).Benefits:
try
/else
syntax could be trivially desugared into the existing "compound if":go
a, b := try SomeFunc() else err {
return 0, errors.Wrap(err, "error in doSomething:")
}
go
if a, b, err := SomeFunc(); err != nil {
return 0, errors.Wrap(err, "error in doSomething:")
}
check
/handle
that "this error handling framework is only good for bailouts". We also get around the "handler chain" criticism. Any arbitrary code can be placed inside one of these handlers, and no other control flow is implied.return
inside the handler to mean super return
. Hijacking a keyword is extremely confusing. return
just means return
, and there's no real need for super return
.defer
doesn't need to moonlight as an error handling mechanism. We can continue to think of it mainly as a way to clean up resources, etc.if err != nil
blocksgo vet
check to highlight unhandled errors.Apologies if these ideas are very similar to other proposals — I've tried to keep up with them all, but may have missed a good deal.
@brynbellomy Thanks for the keyword analysis - that's very helpful information. It does seem that try
as a keyword might be ok. (You say that APIs are not affected - that's true, but try
might still show up as parameter name or the like - so documentation may have to change. But I agree that would not affect clients of those packages.)
Regarding your proposal: It would stand just fine even without named handlers, wouldn't it? (That would simplify the proposal without loss of power. One could simply call a local function from the inlined handler.)
Regarding your proposal: It would stand just fine even without named handlers, wouldn't it? (That would simplify the proposal without loss of power. One could simply call a local function from the inlined handler.)
@griesemer Indeed — I was feeling pretty lukewarm about including those. Certainly more Go-ish without.
On the other hand, it does seem that people want the ability to do one-liner error handling, including one-liners that return
. A typical case would be log, then return
. If we shell out to a local function in the else
clause, we probably lose that:
a, b := try SomeFunc() else err {
someLocalFunc(err)
return 0, err
}
(I still prefer this to compound ifs, though)
However, you could still get one-liner returns that add error context by implementing a simple gofmt
tweak discussed earlier in the thread:
a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }
Is the new keyword necessary in the above proposal? Why not:
SomeFunc() else return
a, b := SomeOtherFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }
@griesemer if handlers are back on the table, I suggest you make a new issue for discussion of try/handle or try/_label_. This proposal specifically omitted handlers, and there are innumerable ways to define and invoke them.
Anyone suggesting handlers should first read the check/handle feedback wiki. Chances are good that whatever you dream up is already described there :-)
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback
@smonkewitz no, a new keyword isn't necessary in that version as its bound to assignment statements, which has been mentioned multiple times thus far in various syntax sugar.
https://github.com/golang/go/issues/32437#issuecomment-499808741
https://github.com/golang/go/issues/32437#issuecomment-499852124
https://github.com/golang/go/issues/32437#issuecomment-500095505
@ianlancetaylor has this particular flavor of error handling been considered by the go team yet? Its not as easy to implement as the proposed try builtin but feels more idiomatic. ~unnecessary statement, sorry.~
I would like to repeat something @deanveloper and a few others have said, but with my own emphasis. In https://github.com/golang/go/issues/32437#issuecomment-498939499 @deanveloper said:
try
is a conditional return. Control flow AND returns are both held up on pedestals in Go. All control flow within a function is indented, and all returns begin withreturn
. Mixing both of these concepts together into an easy-to-miss function call just feels a bit off.
Furthermore, in this proposal try
is a function that returns values, so it may be used as part of a bigger expression.
Some have argued that panic
has already set the precedent for a built in function that changes control flow, but I think panic
is fundamentally different for two reasons:
Try on the other hand:
For these reasons I think try
feels more than a "bit off", I think it fundamentally harms code readability.
Today, when we encounter some Go code for the first time we can quickly skim it to find the possible exit points and control flow points. I believe that is a highly valuable property of Go code. Using try
it becomes too easy to write code lacking that property.
I admit that it is likely that Go developers that value code readability would converge on usage idioms for try
that avoid these readability pitfalls. I hope that would happen since code readability seems to be a core value for many Go developers. But it's not obvious to me that try
adds enough value over existing code idioms to carry the weight of adding a new concept to the language for everyone to learn and that can so easily harm readability.
````
if it != "broke" {
dontFix(it)
}
@ChrisHines To your point (which is echoed elsewhere in this thread), let's add another restriction:
try
statement (even those without a handler) must occur on its own line.You would still benefit from a big reduction in visual noise. Then, you have guaranteed returns annotated by return
and conditional returns annotated by try
, and those keywords always stand at the beginning of a line (or at worst, directly after a variable assignment).
So, none of this type of nonsense:
try EmitEvent(try (try DecodeMsg(m)).SaveToDB())
but rather this:
dm := try DecodeMsg(m)
um := try dm.SaveToDB()
try EmitEvent(um)
which still feels clearer than this:
dm, err := DecodeMsg(m)
if err != nil {
return nil, err
}
um, err := dm.SaveToDB()
if err != nil {
return nil, err
}
err = EmitEvent(um)
if err != nil {
return nil, err
}
One thing I like about this design is that it is impossible to silently ignore errors without still annotating that one might occur. Whereas right now, you sometimes see x, _ := SomeFunc()
(what is the ignored return value? an error? something else?), now you have to annotate clearly:
x := try SomeFunc() else err {}
Since my previous post in support of the proposal, I've seen two ideas posted by @jagv (parameterless try
returns *error
) and by @josharian (labelled error handlers) which I believe in a slightly modified form would enhance the proposal considerably.
Putting theses ideas together with a further one I've had myself, we'd have four versions of try
:
defer
statement.It seems to me that this will usually be a better way of decorating errors (or handling them locally and then returning nil) than defer
as it's simpler and quicker. Anybody who didn't like it could still use #2.
It would be best practice to place the error handling label/code near the end of the function and not to jump back into the rest of the function body. However, I don't think the compiler should enforce either as there might be odd occasions where they're useful and enforcement might be difficult in any case.
So normal label and goto
behavior would apply subject (as @josharian said) to #26058 being fixed first but I think it should be fixed anyway.
The name of the label couldn't be panic
as this would conflict with #4.
panic
immediately rather than returning or branching. Consequently, if this were the only version of try
used in a particular function, no ERP would be required.I've added this so the testing package can work as it does now without the need for a further built-in or other changes. However, it might be useful in other _fatal_ scenarios as well.
This needs to be a separate version of try
as the alternative of branching to an error handler and then panicking from that would still require an ERP.
One of the strongest type of reactions to the initial proposal was concern around losing easy visibility of normal flow of where a function returns.
For example, @deanveloper expressed that concern very well in https://github.com/golang/go/issues/32437#issuecomment-498932961, which I think is the highest upvoted comment here.
@dominikh wrote in https://github.com/golang/go/issues/32437#issuecomment-499067357:
In gofmt'ed code, a return always matches /^\t*return / – it's a very trivial pattern to spot by eye, without any assistance. try, on the other hand, can occur anywhere in the code, nested arbitrarily deep in function calls. No amount of training will make us be able to immediately spot all control flow in a function without tool assistance.
To help with that, @brynbellomy suggested yesterday:
any try statement (even those without a handler) must occur on its own line.
Taking that further, the try
could be required to be the start of the line, even for an assignment.
So it could be:
try dm := DecodeMsg(m)
try um := dm.SaveToDB()
try EmitEvent(um)
rather than the following (from @brynbellomy's example):
dm, err := DecodeMsg(m)
if err != nil {
return nil, err
}
um, err := dm.SaveToDB()
if err != nil {
return nil, err
}
err = EmitEvent(um)
if err != nil {
return nil, err
}
That seems it would preserve a fair amount of visibility, even without any editor or IDE assistance, while still reducing boilerplate.
That could work with the currently proposed defer-based approach that relies on named result parameters, or it could work with specifying normal handler functions. (Specifying handler functions without requiring named return values seems better to me than requiring named return values, but that is a separate point).
The proposal includes this example:
info := try(try(os.Open(file)).Stat()) // proposed try built-in
That could instead be:
try f := os.Open(file)
try info := f.Stat()
That is still a reduction in boilerplate compared to what someone might write today, even if not quite as short as the proposed syntax. Perhaps that would be sufficiently short?
@elagergren-spideroak supplied this example:
try(try(try(to()).parse().this)).easily())
I think that has mismatched parens, which is perhaps a deliberate point or a subtle bit of humor, so I'm not sure if that example intends to have 2 try
or 3 try
. In any event, perhaps it would be better to require spreading that across 2-3 lines that start with try
.
@thepudds, this is what I was getting at in my earlier comment. Except that given
try f := os.Open(file)
try info := f.Stat()
An obvious thing to do is to think of try
as a try block where more than one sentence can be put within parentheses . So the above can become
try (
f := os.Open(file)
into := f.Stat()
)
If the compiler knows how to deal with this, the same thing works for nesting as well. So now the above can become
try info := os.Open(file).Stat()
From function signatures the compiler knows that Open can return an error value and as it is in a try block, it needs to generate error handling and then call Stat() on the primary returned value and so on.
The next thing is to allow statements where either there is no error value being generated or is handled locally. So you can now say
try (
f := os.Open(file)
debug("f: %v\n", f) // debug returns nothing
into := f.Stat()
)
This allows evolving code without having rearrange try blocks. But for some strange reason people seem to think that error handling must be explicitly spelled out! They want
try(try(try(to()).parse()).this)).easily())
While I am perfectly fine with
try to().parse().this().easily()
Even though in both cases exactly the same error checking code can be generated. My view is that you can always write special code for error handling if you need to. try
(or whatever you prefer to call it) simply declutters the default error handling (which is to punt it to the caller).
Another benefit is that if the compiler generates the default error handling, it can add some more identifying information so you know which of the four functions above failed.
I was somewhat concerned about the readability of programs where try
appears inside other expressions. So I ran grep "return .*err$"
on the standard library and started reading through blocks at random. There are 7214 results, I only read a couple hundred.
The first thing of note is that where try
applies, it makes almost all of these blocks a little more readable.
The second thing is that very few of these, less than 1 in 10, would put try
inside another expression. The typical case is statements of the form x := try(...)
or ^try(...)$
.
Here are a few examples where try
would appear inside another expression:
text/template
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
lessOrEqual, err := le(arg1, arg2)
if err != nil {
return false, err
}
return !lessOrEqual, nil
}
becomes:
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
return !try(le(arg1, arg2)), nil
}
text/template
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
switch v.Kind() {
case reflect.Map:
index, err := prepareArg(index, v.Type().Key())
if err != nil {
return reflect.Value{}, err
}
if x := v.MapIndex(index); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
...
}
}
becomes
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
switch v.Kind() {
case reflect.Map:
if x := v.MapIndex(try(prepareArg(index, v.Type().Key()))); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
...
}
}
(this is the most questionable example I saw)
regexp/syntax:
regexp/syntax/parse.go
func Parse(s string, flags Flags) (*Regexp, error) {
...
if c, t, err = nextRune(t); err != nil {
return nil, err
}
p.literal(c)
...
}
becomes
func Parse(s string, flags Flags) (*Regexp, error) {
...
c, t = try(nextRune(t))
p.literal(c)
...
}
This is not an example of try inside another expression but I want to call it out because it improves readability. It's much easier to see here that the values of c
and t
are living beyond the scope of the if statement.
net/http
net/http/request.go:readRequest
mimeHeader, err := tp.ReadMIMEHeader()
if err != nil {
return nil, err
}
req.Header = Header(mimeHeader)
becomes:
req.Header = Header(try(tp.ReadMIMEHeader())
database/sql
if driverCtx, ok := driveri.(driver.DriverContext); ok {
connector, err := driverCtx.OpenConnector(dataSourceName)
if err != nil {
return nil, err
}
return OpenDB(connector), nil
}
becomes
if driverCtx, ok := driveri.(driver.DriverContext); ok {
return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
}
database/sql
si, err := ctxDriverPrepare(ctx, dc.ci, query)
if err != nil {
return nil, err
}
ds := &driverStmt{Locker: dc, si: si}
becomes
ds := &driverStmt{
Locker: dc,
si: try(ctxDriverPrepare(ctx, dc.ci, query)),
}
net/http
if http2isconnectioncloserequest(req) && dialonmiss {
// it gets its own connection.
http2tracegetconn(req, addr)
const singleuse = true
cc, err := p.t.dialclientconn(addr, singleuse)
if err != nil {
return nil, err
}
return cc, nil
}
becomes
if http2isconnectioncloserequest(req) && dialonmiss {
// it gets its own connection.
http2tracegetconn(req, addr)
const singleuse = true
return try(p.t.dialclientconn(addr, singleuse))
}
net/http
func (f *http2Framer) endWrite() error {
...
n, err := f.w.Write(f.wbuf)
if err == nil && n != len(f.wbuf) {
err = io.ErrShortWrite
}
return err
}
becomes
func (f *http2Framer) endWrite() error {
...
if try(f.w.Write(f.wbuf) != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
(This one I really like.)
net/http
if f, err := fr.ReadFrame(); err != nil {
return nil, err
} else {
hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}
becomes
hc = try(fr.ReadFrame()).(*http2ContinuationFrame)// guaranteed by checkFrameOrder
}
(Also nice.)
net:
if ctrlFn != nil {
c, err := newRawConn(fd)
if err != nil {
return err
}
if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
return err
}
}
becomes
if ctrlFn != nil {
try(ctrlFn(fd.ctrlNetwork(), laddr.String(), try(newRawConn(fd))))
}
maybe this is too much, and instead it should be:
if ctrlFn != nil {
c := try(newRawConn(fd))
try(ctrlFn(fd.ctrlNetwork(), laddr.String(), c))
}
Overall, I quite enjoy the effect of try
on the standard library code I read through.
One final point: Seeing try
applied to read code beyond the few examples in the proposal was enlightening. I think it is worth considering writing a tool to automatically convert code to use try
(where it doesn't change the semantics of the program). It would be interesting to read a sample of the diffs is produces against popular packages on github to see if what I found in the standard library holds up. Such a program's output could provide extra insight into the effect of the proposal.
@crawshaw thanks for doing this, it was great to see it in action. But seeing it in action made me take more seriously the arguments against inline error handling that I had until now been dismissing.
Since this was in such close proximity to @thepudds interesting suggestion of making try
a statement, I rewrote all of the examples using that syntax and found it much clearer than either the expression-try
or the status quo, without requiring too many extra lines:
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
try lessOrEqual := le(arg1, arg2)
return !lessOrEqual, nil
}
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
switch v.Kind() {
case reflect.Map:
try index := prepareArg(index, v.Type().Key())
if x := v.MapIndex(index); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
...
}
}
func Parse(s string, flags Flags) (*Regexp, error) {
...
try c, t = nextRune(t)
p.literal(c)
...
}
try mimeHeader := tp.ReadMIMEHeader()
req.Header = Header(mimeHeader)
if driverCtx, ok := driveri.(driver.DriverContext); ok {
try connector := driverCtx.OpenConnector(dataSourceName)
return OpenDB(connector), nil
}
This one would arguably be better with an expression-try
if there were multiple fields that had to be try
-ed, but I still prefer the balance of this trade off
try si := ctxDriverPrepare(ctx, dc.ci, query)
ds := &driverStmt{Locker: dc, si: si}
This is basically the worst-case for this and it looks fine:
if http2isconnectioncloserequest(req) && dialonmiss {
// it gets its own connection.
http2tracegetconn(req, addr)
const singleuse = true
try cc := p.t.dialclientconn(addr, singleuse)
return cc, nil
}
I debated with myself whether if try
would or should be legal, but I couldn't come up with a reasonable explanation why it shouldn't be and it works quite well here:
func (f *http2Framer) endWrite() error {
...
if try n := f.w.Write(f.wbuf); n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
try f := fr.ReadFrame()
hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
if ctrlFn != nil {
try c := newRawConn(fd)
try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
}
Scanning through @crawshaw's examples only makes me feel more sure that control flow will be often made cryptic enough to be even more careful about the design. Relating even a small amount of complexity becomes difficult to read and easy to botch. I'm glad to see options considered, but complicating control flow in such a guarded language seems exceptionally out of character.
func (f *http2Framer) endWrite() error {
...
if try(f.w.Write(f.wbuf) != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
Also, try
is not "trying" anything. It is a "protective relay". If the base semantics of the proposal is off, I'm not surprised the resulting code is also problematic.
func (f *http2Framer) endWrite() error {
...
relay n := f.w.Write(f.wbuf)
return checkShortWrite(n, len(f.wbuf))
}
If you make try a statement, you could use a flag to indicate which return value, and what action:
try c, @ := newRawConn(fd) // return
try c, @panic := newRawConn(fd) // panic
try c, @hname := newRawConn(fd) // invoke named handler
try c, @_ := newRawConn(fd) // ignore, or invoke "ignored" handler if defined
You still need a sub-expression syntax (Russ has stated it's a requirement), at least for panic and ignore actions.
First, I applaud @crawshaw for taking the time to look at roughly 200 real examples and taking the time for his thoughtful write-up above.
Second, @jimmyfrasche, regarding your response here about the http2Framer
example:
I debated with myself whether
if try
would or should be legal, but I couldn't come up with a reasonable explanation why it shouldn't be and it works quite well here:```
func (f *http2Framer) endWrite() error {
...
if try n := f.w.Write(f.wbuf); n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
At least under what I was suggesting above in https://github.com/golang/go/issues/32437#issuecomment-500213884, under that proposal variation I would suggest `if try` is not allowed.
That `http2Framer` example could instead be:
func (f *http2Framer) endWrite() error {
...
try n := f.w.Write(f.wbuf)
if n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
``
That is one line longer, but hopefully still "light on the page". Personally, I think that (arguably) reads more cleanly, but more importantly it is easier to see the
try`.
@deanveloper wrote above in https://github.com/golang/go/issues/32437#issuecomment-498932961:
Returning from a function has seemed to have been a "sacred" thing to do
That specific http2Framer
example ends up being not as short as it possibly could be. However, it holds returning from a function more "sacred" if the try
must be the first thing on a line.
@crawshaw mentioned:
The second thing is that very few of these, less than 1 in 10, would put try inside another expression. The typical case is statements of the form x := try(...) or ^try(...)$.
Maybe it is OK to only partially help those 1 in 10 examples with a more restricted form of try
, especially if the typical case from those examples ends up with the same line count even if try
is required to be the first thing on a line?
@jimmyfrasche
@crawshaw thanks for doing this, it was great to see it in action. But seeing it in action made me take more seriously the arguments against inline error handling that I had until now been dismissing.
Since this was in such close proximity to @thepudds interesting suggestion of making
try
a statement, I rewrote all of the examples using that syntax and found it much clearer than either the expression-try
or the status quo, without requiring too many extra lines:func gt(arg1, arg2 reflect.Value) (bool, error) { // > is the inverse of <=. try lessOrEqual := le(arg1, arg2) return !lessOrEqual, nil }
Your first example illustrates well why I strongly prefer the expression-try
. In your version, I have to put the result of the call to le
in a variable, but that variable has no semantic meaning that the term le
doesn't already imply. So there's no name I can give it that isn't either meaningless (like x
) or redundant (like lessOrEqual
). With expression-try
, no intermediate variable is needed, so this problem doesn't even arise.
I'd rather not have to expend mental effort inventing names for things that are better left anonymous.
I'm happy to lob my support behind the last few posts where try
(the keyword) has been moved to the beginning of the line. It really ought to share the same visual space as return
.
Re: @jimmyfrasche's suggestion to allow try
within compound if
statements, that's exactly the sort of thing I think many here are trying to avoid, for a few reasons:
try
expression is actually evaluated first, and can cause the function to return, yet it appears after the if
try
is actually unhandled, because the block looks a lot like a handler block (even though it's handling a totally different problem)One could approach this situation from a slightly different angle that favors pushing people to handle try
s. How about allowing the try
/else
syntax to contain subsequent conditionals (which is a common pattern with many I/O functions that return both an err
and an n
, either of which might indicate a problem):
func (f *http2Framer) endWrite() error {
// ...
try n := f.w.Write(f.wbuf) else err {
return errors.Wrap(err, "error writing:")
} else if n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
In the case where you don't handle the error returned by .Write
, you'd still have a clear annotation that .Write
might error (as pointed out by @thepudds):
func (f *http2Framer) endWrite() error {
// ...
try n := f.w.Write(f.wbuf)
if n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
I second @daved ‘s response. In my opinion each example that @crawshaw highlighted became less clear and more error prone as a result of try
.
I'm happy to lob my support behind the last few posts where
try
(the keyword) has been moved to the beginning of the line. It really ought to share the same visual space asreturn
.
Given the two options for this point and assuming that one was chosen and thus set somewhat of a precedent for future potential features:
A.)
try f := os.Open(filepath) else err {
return errors.Wrap(err, "can't open")
}
B.)
f := try os.Open(filepath) else err {
return errors.Wrap(err, "can't open")
}
Which of the two provide more flexibility for future new keyword use? _(I do not know the answer to this as I have not mastered the dark art of writing compilers.)_ Would one approach be more limiting than another?
@davecheney @daved @crawshaw
I'd tend to agree with the Daves on this one: in @crawshaw's examples, there are lots of try
statements embedded deep in lines that have a lot of other stuff going on. Really hard to spot exit points. Further, the try
parens seem to clutter things up pretty badly in some of the examples.
Seeing a bunch of stdlib code transformed like this is very useful, so I've taken the same examples but rewritten them per the alternate proposal, which is more restrictive:
try
as a keywordtry
per linetry
must be at the beginning of a lineHopefully this will help us compare. Personally, I find that these examples look a lot more concise than their originals, but without obscuring control flow. try
remains very visible anywhere it's used.
text/template
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
lessOrEqual, err := le(arg1, arg2)
if err != nil {
return false, err
}
return !lessOrEqual, nil
}
becomes:
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
try lessOrEqual := le(arg1, arg2)
return !lessOrEqual, nil
}
text/template
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
switch v.Kind() {
case reflect.Map:
index, err := prepareArg(index, v.Type().Key())
if err != nil {
return reflect.Value{}, err
}
if x := v.MapIndex(index); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
...
}
}
becomes
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
switch v.Kind() {
case reflect.Map:
try index := prepareArg(index, v.Type().Key())
if x := v.MapIndex(index); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
...
}
}
regexp/syntax:
regexp/syntax/parse.go
func Parse(s string, flags Flags) (*Regexp, error) {
...
if c, t, err = nextRune(t); err != nil {
return nil, err
}
p.literal(c)
...
}
becomes
func Parse(s string, flags Flags) (*Regexp, error) {
...
try c, t = nextRune(t)
p.literal(c)
...
}
net/http
net/http/request.go:readRequest
mimeHeader, err := tp.ReadMIMEHeader()
if err != nil {
return nil, err
}
req.Header = Header(mimeHeader)
becomes:
try mimeHeader := tp.ReadMIMEHeader()
req.Header = Header(mimeHeader)
database/sql
if driverCtx, ok := driveri.(driver.DriverContext); ok {
connector, err := driverCtx.OpenConnector(dataSourceName)
if err != nil {
return nil, err
}
return OpenDB(connector), nil
}
becomes
if driverCtx, ok := driveri.(driver.DriverContext); ok {
try connector := driverCtx.OpenConnector(dataSourceName)
return OpenDB(connector), nil
}
database/sql
si, err := ctxDriverPrepare(ctx, dc.ci, query)
if err != nil {
return nil, err
}
ds := &driverStmt{Locker: dc, si: si}
becomes
try si := ctxDriverPrepare(ctx, dc.ci, query)
ds := &driverStmt{Locker: dc, si: si}
net/http
if http2isconnectioncloserequest(req) && dialonmiss {
// it gets its own connection.
http2tracegetconn(req, addr)
const singleuse = true
cc, err := p.t.dialclientconn(addr, singleuse)
if err != nil {
return nil, err
}
return cc, nil
}
becomes
if http2isconnectioncloserequest(req) && dialonmiss {
// it gets its own connection.
http2tracegetconn(req, addr)
const singleuse = true
try cc := p.t.dialclientconn(addr, singleuse)
return cc, nil
}
net/http
This one doesn't actually save us any lines, but I find it much clearer because if err == nil
is a relatively uncommon construction.
func (f *http2Framer) endWrite() error {
...
n, err := f.w.Write(f.wbuf)
if err == nil && n != len(f.wbuf) {
err = io.ErrShortWrite
}
return err
}
becomes
func (f *http2Framer) endWrite() error {
...
try n := f.w.Write(f.wbuf)
if n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
net/http
if f, err := fr.ReadFrame(); err != nil {
return nil, err
} else {
hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}
becomes
try f := fr.ReadFrame()
hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}
net:
if ctrlFn != nil {
c, err := newRawConn(fd)
if err != nil {
return err
}
if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
return err
}
}
becomes
if ctrlFn != nil {
try c := newRawConn(fd)
try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
}
@james-lawrence In reply to https://github.com/golang/go/issues/32437#issuecomment-500116099 : I don't recall ideas like an optional , err
being seriously considered, no. Personally I think it's a bad idea, because it means that if a function changes to add a trailing error
parameter, existing code will continue to compile, but will act very differently.
Using defer for handling the errors make a lot of sense, but it leads to needing to name the error and a new kind of if err != nil
boilerplate.
External handlers need to do this:
func handler(err *error) {
if *err != nil {
*err = handle(*err)
}
}
which gets used like
defer handler(&err)
External handlers need only be written once but there would need to be two versions of many error handling functions: the one meant to be defer'd and the one to be used in the regular fashion.
Internal handlers need to do this:
defer func() {
if err != nil {
err = handle(err)
}
}()
In both cases, the outer function's error must be named to be accessed.
As I mentioned earlier in the thread, this can be abstracted into a single function:
func catch(err *error, handle func(error) error) {
if *err != nil && handle != nil {
*err = handle(*err)
}
}
That runs afoul of @griesemer's concern over the ambiguity of nil
handler funcs and has its own defer
and func(err error) error
boilerplate, in addition to having to name err
in the outer function.
If try
ends up as a keyword, then it could make sense to have a catch
keyword, to be described below, as well.
Syntactically, it would be much like handle
:
catch err {
return handleThe(err)
}
Semantically, it would be sugar for the internal handler code above:
defer func() {
if err != nil {
err = handleThe(err)
}
}()
Since it's somewhat magical, it could grab the outer function's error, even if it was not named. (The err
after catch
is more like a parameter name for the catch
block).
catch
would have the same restriction as try
that it must be in a function that has a final error return, as they're both sugar that relies on that.
That's nowhere near as powerful as the original handle
proposal, but it would obviate the requirement to name an error in order to handle it and it would remove the new boilerplate discussed above for internal handlers while making it easy enough to not require separate versions of functions for external handlers.
Complicated error handling may require not using catch
the same as it may require not using try
.
Since these are both sugar, there's no need to use catch
with try
. The catch
handlers are run whenever the function returns a non-nil
error, allowing, for example, sticking in some quick logging:
catch err {
log.Print(err)
return err
}
or just wrapping all returned errors:
catch err {
return fmt.Errorf("foo: %w", err)
}
@ianlancetaylor
_" I think it's a bad idea, because it means that if a function changes to add a trailing
error
parameter, existing code will continue to compile, but will act very differently."_
That is probably the correct way to look at it, if you are able to control both the upstream and downstream code so that if you need to change a function signature in order to also return an error then you can do so.
But I would ask you to consider what happens when someone does not control either upstream or downstream of their own packages? And also to consider the use-cases where errors might be added, and what happens if errors need to be added but you cannot force downstream code to change?
Can you think of an example where someone would change signature to add a return value? For me they have typically fallen into the category of _"I did not realize an error would occur"_ or _"I'm feeling lazy and don't want to go to the effort because the error probably won't happen."_
In both of those cases I might add an error return because it becomes apparent an error needs to be handled. When that happens, if I cannot change the signature because I don't want to break compatibility for other developers using my packages, what to do? My guess is that the vast majority of the time the error will occur and that the code that called the func that does not return the error will act very differently, _anyway._
Actually, I rarely do the latter but too frequently do the former. But I have noticed 3rd party packages frequently ignore capturing errors where they should be, and I know this because when I bring up their code in GoLand flags in bright orange every instance. I would love to be able to submit pull requests to add error handling to the packages I use a lot, but if I do most won't accept them because I would be breaking their code signatures.
By not offering a backward compatible way to add errors to be returned by functions, developers who distribute code and care about not breaking things for their users won't be able to evolve their packages to include the error handling as they should.
Maybe rather than consider the problem being that code will act different instead view the problem as an engineering challenge regarding how to minimize the downside of a method that is not actively capturing an error? That would have broader and longer term value.
For example, consider adding a package error handler that one must set before being able to ignore errors? Or require a local handler in a function before allowing it?
To be frank, Go's idiom of returning errors in addition to regular return values was one of its better innovations. But as so often happens when you improve things you often expose other weaknesses and I will argue that Go's error handling did not innovate enough.
We Gophers have become steeped in returning an error rather than throwing an exception so the question I have is _"Why shouldn't we been returning errors from every function?"_ We don't always do so because writing code without error handling is more convenient than coding with it. So we omit error handling when we think we can get away from it. But frequently we guess wrong.
So really, if it were possible to figure out how to make the code elegant and readable I would argue that return values and errors really should be handled separately, and that _every_ function should have the ability to return errors regardless of its past function signatures. And getting existing code to gracefully handle code that now generates errors would be a worthwhile endeavor.
I have not proposed anything because I have not been able to envision a workable syntax, but if we want to be honest with ourselves, hasn't everything in this thread and related to Go's error handling in general been about the fact that error handling and program logic are strange bedfellows so ideally errors would be best handled out-of-band in some way?
try
as a keyword certainly helps with readability (vs. a function call) and seems less complex. @brynbellomy @crawshaw thank you for taking the time to write out the examples.
I suppose my general thought is that try
does too much. It solves: call function, assign variables, check error, and return error if exists. I propose we instead cut scope and solve only for conditional return: "return if last arg not nil".
This is probably not a new idea... But after skimming the proposals in the error feedback wiki, I did not find it (does not mean it's not there)
Mini proposal on conditional return
excerpt:
err, thing := newThing(name)
refuse nil, err
I added it to the wiki as well under "alternative ideas"
Doing nothing also seems like a very reasonable option.
@alexhornbake that gives me an idea slightly different that would be more useful
assert(nil, err)
assert(len(a), len(b))
assert(true, condition)
assert(expected, given)
this way it would not just apply to error checking, but to many types of logic errors.
The given would be wrapped in an error and returned.
@alexhornbake
Just as try
is not actually trying, refuse
is not actually "refusing". The common intent here has been that we are setting a "protective relay" (relay
is short, accurate, and alliterative to return
) that "trips" when one of the wired values meets a condition (i.e. is non-nil error). It's a sort of circuit breaker and, I believe, can add value if it's design was limited to uninteresting cases to simply reduce some of the lowest-hanging boilerplate. Anything remotely complex should rely on plain Go code.
I also commend cranshaw for his work looking through the standard library, but I came to a very different conclusion... I think it makes nearly all those code snippets harder to read and more prone to misunderstanding.
req.Header = Header(try(tp.ReadMIMEHeader())
I will very often miss that this can error out. A quick read gets me "ok, set the header to Header of ReadMimeHeader of the thing".
if driverCtx, ok := driveri.(driver.DriverContext); ok {
return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
}
This one, my eyes just cross trying to parse that OpenDB line. There's so much density there... This shows the major problem that all nested function calls have, in that you have to read from the inside out, and you have to parse it in your head in order to figure out where the innermost part is.
Also note that this can return from two different places in the same line.. .you're gonna be debugging, and it's going to say there was an error returned from this line, and the first thing everyone is going to do is try to figure out why OpenDB is failing with this weird error, when it's actually OpenConnector failing (or vice versa).
ds := &driverStmt{
Locker: dc,
si: try(ctxDriverPrepare(ctx, dc.ci, query)),
}
This is a place where the code can fail where previously it would be impossible. Without try
, struct literal construction cannot fail. My eyes will skim over it like "ok, constructing a driverStmt ... moving on.." and it'll be so easy to miss that actually, this can cause your function to error out. The only way that would have been possible before is if ctxDriverPrepare panicked... and we all know that's a case that 1.) should basically never happen and 2.) if it does, it means something is drastically wrong.
Making try a keyword and a statement fixes a lot of my issues with it. I know that's not backwards compatible, but I don't think using a worse version of it is the solution to the backwards compatibility issue.
@daved I'm not sure I follow. Do you dislike the name, or dislike the idea?
Anyway, I posted this here as an alternative... If there is legit interest I can open a new issue for discussion, don't want to pollute this thread (maybe too late?) Thumbs up/down on the original idea will give us a sense... Of course open to alternative names. Important part is "conditional return without trying to handle assignment".
Although I like the catch proposal by @jimmyfrasche, I would like to propose an alternative:
go
handler fmt.HandleErrorf("copy %s %s", src, dst)
would be equivalent to:
go
defer func(){
if(err != nil){
fmt.HandleErrorf(&err,"copy %s %s", src, dst)
}
}()
where err is the last named return value, with type error. However, handlers may also be used when return values are not named. The more general case would be allowed too:
go
handler func(err *error){ *err = fmt.Errorf("foo: %w", *err) }() `
The main problem I have with using named return values (which catch does not solve) is that err is superfluous. When deferring a call to a handler like fmt.HandleErrorf
, there is no reasonable first argument except a pointer to the error return value, why giving the user the option to make a mistake?
Compared with catch, the main difference is that handler makes a bit easier to call predefined handlers at the expense of making more verbose to define them in place. I am not sure this is ideal, but I think it is more in line with the original proposal.
@yiyus catch
, as I defined it doesn't require err
to be named on the function containing the catch
.
In catch err {
, the err
is what the error is named within the catch
block. It's like a function parameter name.
With that, there's no need for something like fmt.HandleErrorf
because you can just use the regular fmt.Errorf
:
func f() error {
catch err {
return fmt.Errorf("foo: %w", err)
}
return errors.New("bar")
}
which returns an error that prints as foo: bar
.
I do not like this approach, because of:
try()
function call interrupts code execution in the parent function.return
keyword, but the code actually returns.A lot of ways of doing handlers are being proposed, but I think they often miss two key requirements:
It has to be significantly different and better than if x, err := thingie(); err != nil { handle(err) }
. I think suggestions along the lines of try x := thingie else err { handle(err) }
don't meet that bar. Why not just say if
?
It should be orthogonal to the existing functionality of defer
. That is, it should be different enough that it is clear that the proposed handling mechanism is needed in its own right without creating weird corner cases when handle and defer interact.
Please keep these desiderata in mind as we discuss alternative mechanisms for try
/handle.
@carlmjohnson I like @jimmyfrasche's catch
idea regarding your point 2 - it's just syntax sugar for a defer
which saves 2 lines and lets you avoid having to name the error return value (which in turn would also require you to name all the other ones if you hadn't already). It doesn't raise an orthogonality issue with defer
, because it is defer
.
echoing what @ubombi said:
try() function call interrupts code execution in the parent function.; there is no return keyword, but the code actually returns.
In Ruby, procs and lambdas are an example of what try
does...A proc is a block of code that its return statement returns not from the block itself, but from the caller.
This is exactly what try
does...it's just a pre-defined Ruby proc.
I think if we were going to go that route, maybe we can actually let the user define their own try
function by introducing proc functions
I still prefer if err != nil
, because it's more readable but I think try
would be more beneficial if the user defined their own proc:
proc try(err *error, msg string) {
if *err != nil {
*err = fmt.Errorf("%v: %w", msg, *err)
return
}
}
And then you can call it:
func someFunc() (string, error) {
err := doSomething()
try(&err, "someFunc failed")
}
The benefit here, is that you get to define error handling in your own terms. And you can also make a proc
exposed, private, or internal.
It's also better than the handle {}
clause in the original Go2 proposal because you can define this only once for the entire codebase and not in each function.
One consideration for readability, is that a func() and a proc() might be called differently such as func()
and proc!()
so that a programmer knows that a proc call might actually return out of the calling function.
@marwan-at-work, shouldn't try(err, "someFunc failed")
be try(&err, "someFunc failed")
in your example?
@dpinela thank you for the correction, updated the code :)
The common practice we are trying to override here is what standard stack unwinding throughout many languages suggests in an exception, (and hence the word "try" was selected...).
But if we could only allow a function(...try() or other) that would jump back two levels in the trace, then
try := handler(err error) { //which corelates with - try := func(err error)
if err !=nil{
//do what ever you want to do when there's an error... log/print etc
return2 //2 levels
}
}
and then a code like
f := try(os.Open(filename))
could do exactly as the proposal advises, but as its a function (or actually a "handler function") the developer will have much more control on what the function does, how it formats the error in different cases, use a similar handler all around the code to handle (lets say) os.Open, instead of wrting fmt.Errorf("error opening file %s ....") every time.
This would also force error handling as if "try" would not be defined - its a compile time error.
@guybrand Having such a two-level return return2
(or "non-local return" as the general concept is called in Smalltalk) would be a nice general-purpose mechanism (also suggested by @mikeschinkel in #32473). But it appears that try
is still needed in your suggestion, so I don't see a reason for the return2
- the try
can just do the return
. It would be more interesting if one could also write try
locally, but that's not possible for arbitrary signatures.
@griesemer
_"so I don't see a reason for the
return2
- thetry
can just do thereturn
."_
One reason — as I pointed out in #32473 _(thanks for the reference)_ — would be to allow multiple levels of break
and continue
, in addition to return
.
Thanks again to everybody for all the new comments; it's a significant time investment to keep up with the discussion and write up extensive feedback. And better even, despite the sometimes passionate arguments, this has been a rather civil thread so far. Thanks!
Here's another quick summary, this time a bit more condensed; apologies to those I didn't mention, forgot, or misrepesented. At this point I think some larger themes are emerging:
1) In general, using a built-in for the try
functionality is felt to be a bad choice: Given that it affects control flow it should be _at least_ a keyword (@carloslenz "prefers making it a statement without parenthesis"); try
as an expression seems not a good idea, it harms readability (@ChrisHines, @jimmyfrasche), they are "returns without a return
". @brynbellomy did an actual analysis of try
used as identifiers; there appear to be very few percentage-wise, so it might be possible to go the keyword route w/o affecting too much code.
2) @crawshaw took some time to analyze a couple hundred use cases from the std library and came to the conclusion that try
as proposed almost always improved readability. @jimmyfrasche came to the opposite conclusion.
3) Another theme is that using defer
for error decoration is not ideal. @josharian points out the defer
's always run upon function return, but if they are here for error decoration, we only care about their body if there's an error, which could be a source of confusion.
4) Many wrote up suggestions for improving the proposal. @zeebo, @patrick-nyt are in supoort of gofmt
formatting simple if
statements on a single line (and be happy with the status quo). @jargv suggested that try()
(without arguments) could return a pointer to the currently "pending" error, which would remove the need to name the error result just so one has access to it in a defer
; @masterada suggested using errorfunc()
instead. @velovix revived the idea of a 2-argument try
where the 2nd argument would be an error handler.
@klaidliadon, @networkimprov are in favor of special "assignment operators" such as in f, # := os.Open()
instead of try
. @networkimprov filed a more comprehensive alternative proposal investigating such approaches (see issue #32500). @mikeschinkel also filed an alternative proposal suggesting to introduce two new general purpose language features that could be used for error handling as well, rather than an error-specific try
(see issue #32473). @josharian revived a possibility we discussed at GopherCon last year where try
doesn't return upon an error but instead jumps (with a goto
) to a label named error
(alternatively, try
might take the name of a target label).
5) On the subject of try
as a keyword, two lines of thoughts have appeared. @brynbellomy suggested a version that might alternatively specify a handler:
a, b := try f()
a, b := try f() else err { /* handle error */ }
@thepudds goes a step further and suggests try
at the beginning of the line, giving try
the same visibility as a return
:
try a, b := f()
Both of these could work with defer
.
@griesemer
Thanks for the ref to @mikeschinkel #32473, it does have a lot in common.
regarding
But it appears that try is still needed in your suggestion
Although my suggestion can be implemented with "any" handler and not a reserved "build in/keyword/expression" I do not think "try()" is a bad idea (and therefore did not down vote it), I'm trying to "broaden it up" - so it would show more upsides, many expected "once go 2.0 is introduced"
I think that may also be the source of the "mixed vibes" you reported on your last summary - its not "try() does not improve error handling" - sure it does, its "waiting for Go 3.0 to solve some other major error handling pains" people stated above, looks like too long :)
I am conducting a survey on "error handling pains" (and sound out some of the pains are merely "I dont use good practices", whereas some I did not even imagine people (mostly coming from other languages) want to do - from cool to WTF).
Hope I can share some interesting results soon.
lastly -Thanks for the amazing work and patience !
Looking simply at the length of the current proposed syntax versus what is available now, the case where the error just needs to be returned without handling it or decorated it is the case where most convenience is gained. An example with my favorite syntax so far:
try a, b := f() else err { return fmt.Errorf("Decorated: %s", err); }
if a,b, err :=f; err != nil { return fmt.Errorf("Decorated: %s", err); }
try a, b := f()
if a,b, err :=f; err != nil { return err; }
So, different from what I thought before, maybe it is simply enough to alter go fmt, at least for the decorated/handled error case. While for the just pass on the error case, something like try might still be desirable as syntactic sugar for this very common use case.
Regarding try else
, I think conditional error functions like fmt.HandleErrorf
(edit: I'm assuming it returns nil when the input is nil) in the initial comment work fine so adding else
is unnecessary.
a, b, err := doSomething()
try fmt.HandleErrorf(&err, "Decorated "...)
Like many others here, I prefer try
to be a statement rather than an expression, mostly because an expression altering control flow is completely alien to Go. Also because this is not an expression, it should be at the beginning of the line.
I also agree with @daved that the name is not appropriate. After all, what we're trying to achieve here is a guarded assignment, so why not use guard
like in Swift and make the else
clause optional? Something like
GuardStmt = "guard" ( Assignment | ShortVarDecl | Expression ) [ "else" Identifier Block ] .
where Identifier
is the error variable name bound in the following Block
. With no else
clause, just return from the current function (and use a defer handler to decorate errors if need be).
I initially didn't like an else
clause because it's just syntactic sugar around the usual assignment followed by if err != nil
, but after seing some of the examples, it just makes sense: using guard
makes the intent clearer.
EDIT: some suggested to use things like catch
to somehow specify different error handlers. I find else
equally viable semantically speaking and it's already in the language.
While I like the try-else statement, how about this syntax?
a, b, (err) := func() else { return err }
Expression try
-else
is a ternary operator.
a, b := try f() else err { ... }
fmt.Println(try g() else err { ... })`
Statement try
-else
is an if
statement.
try a, b := f() else err { ... }
// (modulo scope of err) same as
a, b, err := f()
if err != nil { ... }
Builtin try
with an optional handler can be achieved with either a helper function (below) or not using try
(not pictured, we all know what that looks like).
a, b := try(f(), decorate)
// same as
a, b := try(g())
// where g is
func g() (whatever, error) {
x, err := f()
if err != nil {
try(decorate(err))
}
return x, nil
}
All three cut down on the boilerplate and help contain the scope of errors.
It gives the most savings for builtin try
but that has the issues mentioned in the design doc.
For statement try
-else
, it provides an advantage over using if
instead of try
. But the advantage is so marginal that I have a hard time seeing it justify itself, though I do like it.
All three assume that it is common to need special error handling for individual errors.
Handling all errors equally can be done in defer
. If the same error handling is being done in each else
block that's a bit repetitious:
func printSum(a, b string) error {
try x := strconv.Atoi(a) else err {
return fmt.Errorf("printSum: %w", err)
}
try y := strconv.Atoi(b) else err {
return fmt.Errorf("printSum: %w", err)
}
fmt.Println("result:", x + y)
return nil
}
I certainly know that there are times when a certain error requires special handling. Those are the instances that stick out in my memory. But, if that only happens, say, 1 out of 100 times, wouldn't it be better to keep try
simple and just not use try
in those situations? On the other hand, if it's more like 1 out of 10 times, adding else
/handler seems more reasonable.
It would be interesting to see an actual distribution of how often try
without an else
/handler vs try
with an else
/handler would be useful, though that's not easy data to gather.
I want to expand on @jimmyfrasche 's recent comment.
The goal of this proposal is to reduce the boilerplate
a, b, err := f()
if err != nil {
return nil, err
}
This code is easy to read. It's only worth extending the language if we can achieve a considerable reduction in boilerplate. When I see something like
try a, b := f() else err { return nil, err }
I can't help but feel that we aren't saving that much. We're saving three lines, which is good, but by my count we're cutting back from 56 to 46 characters. That's not much. Compare to
a, b := try(f())
which cuts from 56 to 18 characters, a much more significant reduction. And while the try
statement makes the potential change of flow of control more clear, overall I don't find the statement more readable. Though on the plus side the try
statement makes it easier to annotate the error.
Anyhow, my point is: if we're going to change something, it should significantly reduce boilerplate or should be significantly more readable. The latter is pretty hard, so any change needs to really work on the former. If we get only a minor reduction in boilerplate, then in my opinion it's not worth doing.
Like others, I would like to thank @crawshaw for the examples.
When reading those examples, I encourage people to try to adopt a mindset in which you don't worry about the flow of control due to the try
function. I believe, perhaps incorrectly, that that flow of control will quickly become second nature to people who know the language. In the normal case, I believe that people will simply stop worrying about what happens in the error case. Try reading those examples while glazing over try
just as you already glaze over if err != nil { return err }
.
After reading through everything here, and upon further reflection, I am not sure I see try even as a statement something worth adding.
the rationale for it seems to be reducing error handling boiler plate code. IMHO it "declutters" the code but it doesn't really remove the complexity; it just obscures it. This doesn't seem strong enough of a reason. The "go
its name doesn't reflect its function. In its simplest form, what it does is this: "if a function returns an error, return from the caller with an error" but that is too long :-) At the very least a different name is needed.
with try's implicit return on error, it feels like Go is sort of reluctantly backing into exception handling. That is if A calls be in a try guard and B calls C in a try guard, and C calls D in a try guard, if D returns an error in effect you have caused a non-local goto. It feels too "magical".
and yet I believe a better way may be possible. Picking try now will close that option off.
@ianlancetaylor
If I understand "try else" proposal correctly, it seems that else
block is optional, and reserved for user provided handling. In your example try a, b := f() else err { return nil, err }
the else
clause is actually redundant, and the whole expression can be written simply as try a, b := f()
I agree with @ianlancetaylor ,
Readability and boilerplate are two main concerns and perhaps the drive to
the go 2.0 error handling (though I can add some other important concerns)
Also, that the current
a, b, err := f()
if err != nil {
return nil, err
}
Is highly readable.
And since I believe
if a, b, err := f(); err != nil {
return nil, err
}
Is almost as readable, yet had it's scope "issues", perhaps a
ifErr a, b, err := f() {
return nil, err
}
That would only the ; err != nil part, and would not create a scope, or
similarly
try a, b, err := f() {
return nil, err
}
Keeps the extra two lines, but is still readable.
On Tue, 11 Jun 2019, 20:19 Dmitriy Matrenichev, notifications@github.com
wrote:
@ianlancetaylor https://github.com/ianlancetaylor
If I understand "try else" proposal correctly, it seems that else block
is optional, and reserved for user provided handling. In your example
try a, b := f() else err { return nil, err } the else clause is actually
redundant, and the whole expression can be written simply as try a, b :=
f()—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4XPURMASWKZKOBPBVDPZ7NALA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXN3VDA#issuecomment-500939404,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABNEY4SAFK4M5NLABF3NZO3PZ7NALANCNFSM4HTGCZ7Q
.
@ianlancetaylor
Anyhow, my point is: if we're going to change something, it should significantly reduce boilerplate or should be significantly more readable. The latter is pretty hard, so any change needs to really work on the former. If we get only a minor reduction in boilerplate, then in my opinion it's not worth doing.
Agreed, and considering that an else
would only be syntactic sugar (with a weird syntax!), very likely used only rarely, I don't care much about it. I'd still prefer try
to be a statement though.
@ianlancetaylor Echoing @DmitriyMV, the else
block would be optional. Let me throw in an example that illustrates both (and doesn't seem too far off the mark in terms of the relative proportion of handled vs. non-handled try
blocks in real code):
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
headRef, err := r.Head()
if err != nil {
return err
}
parentObjOne, err := headRef.Peel(git.ObjectCommit)
if err != nil {
return err
}
parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit)
if err != nil {
return err
}
parentCommitOne, err := parentObjOne.AsCommit()
if err != nil {
return err
}
parentCommitTwo, err := parentObjTwo.AsCommit()
if err != nil {
return err
}
treeOid, err := index.WriteTree()
if err != nil {
return err
}
tree, err := r.LookupTree(treeOid)
if err != nil {
return err
}
remoteBranchName, err := remoteBranch.Name()
if err != nil {
return err
}
userName, userEmail, err := r.UserIdentityFromConfig()
if err != nil {
userName = ""
userEmail = ""
}
var (
now = time.Now()
author = &git.Signature{Name: userName, Email: userEmail, When: now}
committer = &git.Signature{Name: userName, Email: userEmail, When: now}
message = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
parents = []*git.Commit{
parentCommitOne,
parentCommitTwo,
}
)
_, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
if err != nil {
return err
}
return nil
}
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)
try remoteBranchName := remoteBranch.Name()
try userName, userEmail := r.UserIdentityFromConfig() else err {
userName = ""
userEmail = ""
}
var (
now = time.Now()
author = &git.Signature{Name: userName, Email: userEmail, When: now}
committer = &git.Signature{Name: userName, Email: userEmail, When: now}
message = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
parents = []*git.Commit{
parentCommitOne,
parentCommitTwo,
}
)
try r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
return nil
}
While the try
/else
pattern doesn't save many characters over compound if
, it does:
try
if
s suffer fromUnhandled try
will likely be the most common, though.
@ianlancetaylor
Try reading those examples while glazing over try just as you already glaze over if err != nil { return err }.
I don't think that's possible/equatable. Missing that a try exists in a crowded line, or what it exactly wraps, or that there are multiple instances in one line... These are not the same as easily/quickly marking a return point and not worrying about the specifics therein.
@ianlancetaylor
When I see a stop sign, I recognize it by shape and color more than by reading the word printed on it and pondering its deeper implications.
My eyes may glaze over if err != nil { return err }
but at the same time it still registers—clearly and instantly.
What I like about the try
-statement variant is that it reduces the boilerplate but in a way that is both easy to glaze over but hard to miss.
It may mean an extra line here or there but that's still fewer lines than the status quo.
@brynbellomy
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
if headRef, err := r.Head(); err != nil {
return err
} else if parentObjOne, err := headRef.Peel(git.ObjectCommit); err != nil {
return err
} else parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit); err != nil {
return err
} ...
is not so different readability wise, yet (or fmt.Errorf("error with getting head : %s", err.Error() ) allows you to modify and give extra data easily.
What's still a nag is the
Sorry if someone already posted something like this (there are a lot of good ideas :P ) How about this alternative syntax:
fail := func(err error) error {
log.Print("unexpected error", err)
return err
}
a, b, err := f1() // normal
c, d := f2() -> throw // calls throw(err)
e, f := f3() -> panic // calls panic(err)
g, h := f4() -> t.Error // calls t.Error(err)
i, j := f5() -> fail // calls fail(err)
that is, you have a -> handler
on the right of a function call which is called if the returned err != nil. The handler is any function which accepts an error as a single argument and optionally returns an error (i.e., func(error)
or func(error) error
). If the handler returns a nil error the function continues otherwise the error is returned.
so a := b() -> handler
is equivalent to:
a, err := b()
if err != nil {
if herr := handler(err); herr != nil {
return herr
}
}
Now, as a shortcut you could support a try
builtin (or keyword or ?=
operator or whatever) which is short for a := b() -> throw
so you could write something like:
func() error {
a, b := try(f1())
c, d := try(f2())
e, f := try(f3())
...
return nil
}() -> panic // or throw/fail/whatever
Personally I find a ?=
operator easier to read than a try keyword/builtin:
func() error {
a, b ?= f1()
c, d ?= f2()
e, f ?= f3()
...
return nil
}() -> panic
note: here I'm using throw as a placeholder for a builtin that would return the error to the caller.
I haven't commented on the error handling proposals so far because i'm generally in favour, and i like the way they're heading. Both the try function defined in the proposal and the try statement proposed by @thepudds seem like they would be reasonable additions to the language. I'm confident that whatever the Go team comes up with will be a good.
I want to bring up what i see as a minor issue with the way try is defined in the proposal and how it might impact future extensions.
Try is defined as a function taking a variable number of arguments.
func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)
Passing the result of a function call to try
as in try(f())
works implicitly due to the way multiple return values work in Go.
By my reading of the proposal, the following snippets are both valid and semantically equivalent.
a, b = try(f())
//
u, v, err := f()
a, b = try(u, v, err)
The proposal also raises the possibility of extending try
with extra arguments.
If we determine down the road that having some form of explicitly provided error handler function, or any other additional parameter for that matter, is a good idea, it is trivially possible to pass that additional argument to a try call.
Suppose we want to add a handler argument. It can either go at the beginning or end of the argument list.
var h handler
a, b = try(h, f())
// or
a, b = try(f(), h)
Putting it at the beginning doesn't work, because (given the semantics above) try
wouldn't be able to distinguish between an explicit handler argument and a function that returns a handler.
func f() (handler, error) { ... }
func g() (error) { ... }
try(f())
try(h, g())
Putting it at the end would probably work, but then try would be unique in the language as being the only function with a varargs parameter at the beginning of the argument list.
Neither of these problems is a showstopper, but they do make try
feel inconsistent with the rest of the language, and so i'm not sure try
would be easy to extend in the future as the proposal states.
@magical
Having a handler is powerful, perhaps:
I you already declared h,
you can
var h handler
a, b, h = f()
or
a, b, h.err = f()
if its a function-like:
h:= handler(err error){
log(...)
return ....
}
Then there was asuggetion to
a, b, h(err) = f()
All can invoke the handler
And you can also "select" handler that returns or only captures the error (conitnue/break/return) as some suggested.
And thus the varargs issue is gone.
One alternative to @brynbellomy’s else
suggestion of:
a, b := try f() else err { /* handle error */ }
could be to support a decoration function immediately after the else:
decorate := func(err error) error { return fmt.Errorf("foo failed: %v", err) }
try a, b := f() else decorate
try c, d := g() else decorate
And maybe also some utility functions something along the lines of:
decorate := fmt.DecorateErrorf("foo failed")
The decoration function could have signature func(error) error
, and be called by try in the presence of an error, just before try returns from the associated function being tried.
That would be similar in spirit to one of the earlier “design iterations” from the proposal document:
f := try(os.Open(filename), handler) // handler will be called in error case
If someone wants something more complex or a block of statements, they could instead use if
(just as they can today).
That said, there is something nice about the visual alignment of try
shown in @brynbellomy’s example in https://github.com/golang/go/issues/32437#issuecomment-500949780.
All of this could still work with defer
if that approach gets chosen for uniform error decoration (or even in theory there could be an alternative form of registering a decoration function, but that is a separate point).
In any event, I’m not sure what is best here, but wanted to make another option explicit.
Here's @brynbellomy's example rewritten with the try
function, using a var
block to retain the nice alignment that @thepudds pointed out in https://github.com/golang/go/issues/32437#issuecomment-500998690.
package main
import (
"fmt"
"time"
)
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
var (
headRef = try(r.Head())
parentObjOne = try(headRef.Peel(git.ObjectCommit))
parentObjTwo = try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne = try(parentObjOne.AsCommit())
parentCommitTwo = try(parentObjTwo.AsCommit())
treeOid = try(index.WriteTree())
tree = try(r.LookupTree(treeOid))
remoteBranchName = try(remoteBranch.Name())
)
userName, userEmail, err := r.UserIdentityFromConfig()
if err != nil {
userName = ""
userEmail = ""
}
var (
now = time.Now()
author = &git.Signature{Name: userName, Email: userEmail, When: now}
committer = &git.Signature{Name: userName, Email: userEmail, When: now}
message = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
parents = []*git.Commit{
parentCommitOne,
parentCommitTwo,
}
)
_, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
return err
}
It's as succinct as the try
-statement version, and I would argue just as readable. Since try
is an expression, a few of those intermediate variables could be eliminated, at the cost of some readability, but that seems more like a matter of style than anything else.
It does raise the question of how try
works in a var
block, though. I assume each line of the var
counts as a separate statement, rather than the whole block being a single statement, as far as the order of what gets assigned when.
It would be good if the "try" proposal explicitly called out the consequences for tools such as cmd/cover that approximate test coverage stats using naive statement counting. I worry that the invisible error control flow might result in undercounting.
@thepudds
try a, b := f() else decorate
Perhaps its too deep a burn in my brain cells, but this hits me too much as a
try a, b := f() ;catch(decorate)
and a slippery slope to a
a, b := f()
catch(decorate)
I think you can see where that's leading, and for me comparing
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)
try remoteBranchName := remoteBranch.Name()
with
try (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
remoteBranchName := remoteBranch.Name()
)
(or even a catch at the end)
The second is more readable, but emphasizes the fact the functions below return 2 vars, and we magically discard one, collecting it into a "magic returned err" .
try(err) (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
remoteBranchName := remoteBranch.Name()
); err!=nil {
//handle the err
}
at least explicitly sets the variable to return, and let me handle it within the function, whenever I want.
Just interjecting a specific comment since I did not see anyone else explicitly mention it, specifically about changing gofmt
to support the following single line formatting, or any variant:
if f() { return nil, err }
Please, no. If we want a single line if
then please make a single line if
, e.g.:
if f() then return nil, err
But please, please, please do not embrace syntax salad removing the line breaks that make it easier to read code that uses braces.
I like to emphasize a couple of things that may have been forgotten in the heat of the discussion:
1) The whole point of this proposal is to make common error handling fade into the background - error handling should not dominate the code. But should still be explicit. Any of the alternative suggestions that make the error handling sticking out even more are missing the point. As @ianlancetaylor already said, if these alternative suggestions do not reduce the amount of boilerplate significantly, we can just stay with the if
statements. (And the request to reduce boilerplate comes from you, the Go community.)
2) One of the complaints about the current proposal is the need to name the error result in order to get access to it. Any alternative proposal will have the same problem unless the alternative introduces extra syntax, i.e., more boilerplate (such as ... else err { ... }
and the like) to explicitly name that variable. But what is interesting: If we don't care about decorating an error and do not name the result parameters, but still require an explicit return
because there's an explicit handler of sorts, that return
statement will have to enumerate all the (typically zero) result values since a naked return is not permitted in this case. Especially if a function does a lot of error returns w/o decorating the error, those explicit returns (return nil, err
, etc.) add to the boilerplate. The current proposal, and any alternative that doesn't require an explicit return
does away with that. On the other hand, if one does want to decorate the error, the current proposal _requires_ that one name the error result (and with that all the other results) to get access to the error value. This has the nice side effect that in an explicit handler one can use a naked return and does not have to repeat all the other result values. (I know there are some strong feelings about naked returns, but the reality is that when all we care about is the error result, it's a real nuisance to have to enumerate all the other (typically zero) result values - it adds nothing to the understanding of the code). In other words, having to name the error result so that it can be decorated enables further reduction of boilerplate.
@magical Thanks for pointing this out. I noticed the same shortly after posting the proposal (but didn't bring it up to not further cause confusion). You are correct that as is, try
couldn't be extended. Luckily the fix is easy enough. (As it happens our earlier internal proposals didn't have this problem - it got introduced when I rewrote our final version for publication and tried to simplify try
to match existing parameter passing rules more closely. It seemed like a nice - but as it turns out, flawed, and mostly useless - benefit to be able to write try(a, b, c, handle)
.)
An earlier version of try
defined it roughly as follows: try(expr, handler)
takes one (or perhaps two) expressions as arguments, where the first expression may be multi-valued (can only happen if the expression is a function call). The last value of that (possibly multi-valued) expression must be of type error
, and that value is tested against nil. (etc. - the rest you can imagine).
Anyway, the point is that try
syntactically accepts only one, or perhaps two expressions. (But it's a bit harder to describe the semantics of try
.) The consequence would be that code such as:
a, b := try(u, v, err)
would not be permitted anymore. But there is little reason for making this work in the first place: In most cases (unless a
and b
are named results) this code - if important for some reason - could be rewritten easily into
a, b := u, v // we don't care if the assignment happens in case of an error
try(err)
(or use an if
statement as needed). But again, this seems unimportant.
that return statement will have to enumerate all the (typically zero) result values since a naked return is not permitted in this case
A naked return is not permitted, but try would be. One thing I like about try (either as a function or a statement) is that I will not need to think how to set non error values when returning an error any more, I will just use try.
@griesemer Thanks for the explanation. That's the conclusion i came to as well.
A brief comment on try
as a statement: as I think can be seen in the example in https://github.com/golang/go/issues/32437#issuecomment-501035322, the try
buries the lede. Code becomes a series of try
statements, which obscures what the code is actually doing.
Existing code may reuse a newly declared error variable after the if err != nil
block. Hiding the variable would break that, and adding a named return variable to the function signature won't always fix it.
Maybe it's best to leave error declaration/assignment as is, and find a one-line error handling stmt.
err := f() // followed by one of
on err, return err // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err) // doesn't stop the function
on err, hname err // handler invocation without parens
on err, ignore err // optional ignore handler logs error if defined
if err, return err // alternatively, use if
handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
if err == io.Bad { return err }
fmt.Println(clr.name, err)
}
A try
subexpression could panic, meaning error is never expected. A variant of that could ignore any error.
f(try g()) // panic on error
f(try_ g()) // ignore any error
The whole point of this proposal is to make common error handling fade into the background - error handling should not dominate the code. But should still be explicit.
I like the idea of the comments listing try
as a statement. It's explicit, still easy to gloss over (since it is of fixed length), but not _so_ easy to gloss over (since it's always in the same place) that they can be hidden away in a crowded line. It can also be combined with the defer fmt.HandleErrorf(...)
as noted before, however it does have the pitfall of abusing named parameters in order to wrap errors (which still seems like a clever hack to me. clever hacks are bad.)
One of the reasons I did not like try
as an expression is that it's either too easy to gloss over, or not easy enough to gloss over. Take the following two examples:
// Too hidden, it's in a crowded function with many symbols that complicate the function.
f, err := os.OpenFile(try(FileName()), os.O_APPEND|os.O_WRONLY, 0600)
// Not easy enough, the word "try" already increases horizontal complexity, and it
// being an expression only encourages more horizontal complexity.
// If this code weren't collapsed to multiple lines, it would be extremely
// hard to read and unclear as to what's executing when.
fullContents := try(io.CopyN(
os.Stdout,
try(net.Dial("tcp", "localhost:"+try(buf.ReadString("\n"))),
try(strconv.Atoi(try(buf.ReadString("\n")))),
))
// easy to see while still not being too verbose
try name := FileName()
os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)
// does not allow for clever one-liners, code remains much more readable.
// also, the code reads in sequence from top-to-bottom.
try port := r.ReadString("\n")
try lengthStr := r.ReadString("\n")
try length := strconv.Atoi(lengthStr)
try con := net.Dial("tcp", "localhost:"+port)
try io.CopyN(os.Stdout, con, length)
This code is definitely contrived, I'll admit. But what I am getting at is that, in general, try
as an expression doesn't work well in:
I however agree with @ianlancetaylor that beginning each line with try
does seem to get in the way of the important part of each statement (the variable being defined or the function being executed). However I think because it's in the same location and is a fixed width, it is much easier to gloss over, while still noticing it. However, everybody's eyes are different.
I also think encouraging clever one-liners in code is just a bad idea in general. I'm surprised that I could craft such a powerful one-liner as in my first example, it's a snippet that deserves its own entire function because it is doing so much - but it fits on one line if I hadn't of collapsed it to multiple for readability's sake. All in one line:
fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))
It reads a port from a *bufio.Reader
, starts a TCP connection, and copies a number of bytes specified by the same *bufio.Reader
to stdout
. All with error handling. For a language with such strict coding conventions, I don't think this should really even be allowed. I guess gofmt
could help with this, though.
For a language with such strict coding conventions, I don't think this should really even be allowed.
It is possible to write abominable code in Go. It is even possible to format it terribly; there are just strong norms and tools against it. Go even has goto
.
During code reviews, I sometimes ask people to break complicated expressions into multiple statements, with useful intermediate names. I would do something similar for deeply nested try
s, for the same reason.
Which is all to say: Let’s not try too hard to outlaw bad code, at the cost of distorting the language. We have other mechanisms for keeping code clean that are better suited for something that fundamentally involves human judgement on a case by case basis.
It is possible to write abominable code in Go. It is even possible to format it terribly; there are just strong norms and tools against it. Go even has goto.
During code reviews, I sometimes ask people to break complicated expressions into multiple statements, with useful intermediate names. I would do something similar for deeply nested trys, for the same reason.
Which is all to say: Let’s not try too hard to outlaw bad code, at the cost of distorting the language. We have other mechanisms for keeping code clean that are better suited for something that fundamentally involves human judgement on a case by case basis.
This is a good point. We shouldn't outlaw a good idea just because it can be used to make bad code. However, I think that if we have an alternative that promotes better code, it may be a good idea. I really haven't seen much talk _against_ the raw idea behind try
as a statement (without all the else { ... }
junk) until @ianlancetaylor's comment, however I may have just missed it.
Also, not everyone has code reviewers, some people (especially in the far future) will have to maintain unreviewed Go code. Go as a language normally does a very good job of making sure that almost all written code is well-maintainable (at least after a go fmt
), which is not a feat to overlook.
That being said, I am being awfully critical of this idea when it really isn't horrible.
Try as a statement does reduce the boilerplate significantly, and more than try as an expression, if we allow it to work on a block of expressions as was proposed before, even without allowing an else block or an error handler. Using this, deandeveloper's example becomes:
try (
name := FileName()
file := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)
port := r.ReadString("\n")
lengthStr := r.ReadString("\n")
length := strconv.Atoi(lengthStr)
con := net.Dial("tcp", "localhost:"+port)
io.CopyN(os.Stdout, con, length)
)
If the goal is to reduce the if err!= nil {return err}
boilerplate, then I think statement try that allows to take a block of code has the most potential to do that, without becoming unclear.
@beoran At that point, why having try at all? Just allow an assignment where the last error value is missing and make it behave like if it was a try statement (or function call). Not that I am proposing it, but it would reduce boilerplate even more.
I think that the boilerplate would be efficiently reduced by these var blocks, but I fear it may lead to a huge amount of code becoming indented an additional level, which would be unfortunate.
@deanveloper
fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))
I must admit not readable for me, I would probably feel I must:
fullContents := try(io.CopyN(os.Stdout,
try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))),
try(strconv.Atoi(try(r.ReadString("\n"))))))
or similar, for readability, and then we are back with a "try" at the beginning of every line, with indentation.
Well, I think we'd still need the try for backwards compatibility, and also to be explicit about a return that can happen in the block. But note that I'm just following the logic of reducing the boiler plate and then seeing where it leads us. There's always a tension between reducing boilerplate and clarity. I think the main issue at hand in this issue is that we all seem to disagree where the balance should be.
As for the indents, that's what go fmt is for, so personally I don't feel it's much of a problem.
I'd like to join the fray to mention another two possibilities, each of which are independent, so I'll keep them in separate posts.
I thought the suggestion that try()
(with no arguments) could be defined to return a pointer to the error return variable was an interesting one, but I wasn't keen on that kind of punning - it smacks of function overloading, something that Go avoids.
However I liked the general idea of a predefined identifier that refers to the local error value.
So, how about predefining the err
identifier itself to be an alias for the error return variable? So this would be valid:
func foo() error {
defer handleError(&err, etc)
try(something())
}
It would be functionally identical to:
func foo() (err error) {
defer handleError(&err, etc)
try(something())
}
The err
identifier would be defined at universe scope, even though it acts as a function-local alias, so any package-level definition or function-local definition of err
would override it. This might seem dangerous but I scanned the 22m lines of Go in the Go corpus and it's very rare. There are only 4 distinct instances err
used as a global (all as a variable, not a type or constant) - this is something that vet
could warn about.
It's possible that there may be two function error return variables in scope; in this case, I think it's best that the compiler would complain that there's an ambiguity and require the user to explicitly name the correct return variable. So this would be invalid:
func foo() error {
f := func() error {
defer handleError(&err, etc)
try(something())
return nil
}
return f()
}
but you could always write this instead:
func foo() error {
f := func() (err error) {
defer handleError(&err, etc)
try(something())
return nil
}
return f()
}
On the subject of try
as a predefined identifier rather than an operator,
I found myself trending towards a preference for the latter after repeatedly getting the brackets wrong when writing out:
try(try(os.Create(filename)).Write(data))
Under "Why can't we use ? like Rust", the FAQ says:
So far we have avoided cryptic abbreviations or symbols in the language, including unusual operators such as ?, which have ambiguous or non-obvious meanings.
I'm not entirely sure that's true. The .()
operator is unusual until you know Go, as are the channel operators. If we added a ?
operator, I believe that it would shortly become ubiquitous enough that it wouldn't be a significant barrier.
The Rust ?
operator is added after the closing bracket of a function call though, and that means that it's easy to miss when the argument list is long.
How about adding ?()
as an call operator:
So instead of:
x := try(foo(a, b))
you'd do:
x := foo?(a, b)
The semantics of ?()
would be very similar to those of the proposed try
built-in. It would act like a function call except that the function or method being called must return an error as its last argument. As with try
, if the error is non-nil, the ?()
statement will return it.
It seems like the discussion has gotten focused enough that we're now circling around a series of well-defined and -discussed tradeoffs. This is heartening, at least to me, since compromise is very much in the spirit of this language.
@ianlancetaylor I'll absolutely concede that we'll end up with dozens of lines prefixed by try
. However, I don't see how that's worse than dozens of lines postfixed by a two-to-four line conditional expression explicitly stating the same return
expression. Actually, try
(with else
clauses) makes it a bit easier to spot when an error handler is doing something special/non-default. Also, tangentially, re: conditional if
expressions, I think that they bury the lede more than the proposed try
-as-a-statement: the function call lives on the same line as the conditional, the conditional itself winds up at the very end of an already-crowded line, and the variable assignments are scoped to the block (which necessitates a different syntax if you need those variables after the block).
@josharian I've had this thought quite a bit recently. Go strives for pragmatism, not perfection, and its development frequently seems to be data-driven rather than principles-driven. You can write terrible Go, but it's usually harder than writing decent Go (which is good enough for most people). Also worth pointing out — we have many tools to combat bad code: not just gofmt
and go vet
, but our colleagues, and the culture that this community has (very carefully) crafted to guide itself. I would hate to steer clear of improvements that help the general case simply because someone somewhere might footgun themselves.
@beoran This is elegant, and when you think about it, it's actually semantically different from other languages' try
blocks, as it has only one possible outcome: returning from the function with an unhandled error. However: 1) this is probably confusing to new Go coders who have worked with those other languages (honestly not my biggest concern; I trust in the intelligence of programmers), and 2) this will lead to huge amounts of code being indented across many codebases. As far as my code is concerned, I even tend to avoid the existing type
/const
/var
blocks for this reason. Also, the only keywords that currently allow blocks like this are definitions, not control statements.
@yiyus I disagree with removing the keyword, as explicitness is (in my opinion) one of Go's virtues. But I would agree that indenting huge amounts of code to take advantage of try
expressions is a bad idea. So maybe no try
blocks at all?
@rogpeppe I think that kind of subtle operator is only reasonable for calls that should never return error, and so panic if they do. Or calls where you always ignore the error. But both seem to be rare. If you're open to a new operator, see #32500.
I suggested that f(try g())
should panic in https://github.com/golang/go/issues/32437#issuecomment-501074836, along with a 1-line handling stmt:
on err, return ...
I think the optional else
in try ... else { ... }
will push code too much to the right, possibly obscuring it. I expect the error block should take at least 25 chars most of the time. Also, up until now blocks are not kept on the same line by go fmt
and I expect this behavior will be kept for try else
. So we should be discussing and comparing samples where the else
block is on a separate line. But even then I am not sure about the readability of else {
at the end of the line.
@yiyus https://github.com/golang/go/issues/32437#issuecomment-501139662
@beoran At that point, why having try at all? Just allow an assignment where the last error value is missing and make it behave like if it was a try statement (or function call). Not that I am proposing it, but it would reduce boilerplate even more.
That can't be done because Go1 already allows calling a func foo() error
as just foo()
. Adding , error
to the return values of the caller would change behavior of existing code inside that function. See https://github.com/golang/go/issues/32437#issuecomment-500289410
@rogpeppe In your comment about getting the parentheses right with nested try
's: Do you have any opinions on the precedence of try
? See also the detailed design doc on this subject.
@griesemer I'm indeed not that keen on try
as a unary prefix operator for the reasons pointed out there. It has occurred to me that an alternative approach would be to allow try
as a pseudo-method on a function return tuple:
f := os.Open(path).try()
That solves the precedence issue, I think, but it's not really very Go-like.
@rogpeppe
Very interesting! . You may really be on to something here.
And how about we extend that idea like so?
for _,fp := range filepaths {
f := os.Open(path).try(func(err error)bool{
fmt.Printf( "Cannot open file %s\n", fp );
continue;
});
}
BTW, I might prefer a different name vs try()
such as maybe guard()
but I shouldn't bikeshed the name prior to the architecture being discussed by others.
vs :
for _,fp := range filepaths {
if f,err := os.Open(path);err!=nil{
fmt.Printf( "Cannot open file %s\n", fp )
}
}
?
I like the try a,b := foo()
instead of if err!=nil {return err}
because it replace a boilerplate for really simple case. But for everything else which add context do we really need something else than if err!=nil {...}
(it will be very difficult to find better) ?
If an extra line is usually required for decoration/wrap, let's just "allocate" a line for it.
f, err := os.Open(path) // normal Go \o/
on err, return fmt.Errorf("Cannot open %s, due to %v", path, err)
@networkimprov I think I could like that as well. Pushing a more alliterative and descriptive term I already brought up...
f, err := os.Open(path)
relay err { nil, fmt.Errorf("Cannot open %s, due to %v", path, err) }
// marginally shorter, doesn't trigger vertical formatting unless excessively wide
// enclosed expression restricted to a list of values that match the return args
or
f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)
// somewhere between statement and func, prob more pleasing to type w/out completion
// trailing expression restricted to a list of values that match the return args
// maybe excessive width triggers linting noise - with a reformatter available
// providing a reformatter would make swapping old (narrow enough) code easy
@daved glad you like it! on err, ...
would allow any single-stmt handler:
err := f() // followed by one of
on err, return err // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err) // doesn't stop the function
on err, continue // retry in a loop
on err, hname err // named handler invocation without parens
on err, ignore err // logs error if handle ignore() defined
handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
if err == io.Bad { return err } // non-local return
fmt.Println(clr, err)
}
EDIT: on
borrows from Javascript. I didn't want to overload if
.
A comma isn't essential, but I don't like semicolon there. Maybe colon?
I don't quite follow relay
; it means return-on-error?
A protective relay is tripped when some condition is met. In this case, when an error value is not nil, the relay alters the control flow to return using the subsequent values.
*I wouldn't want to overload ,
for this case, and am not a fan of the term on
, but I like the premise and overall look of the code structure.
To @josharian's point earlier, I feel like a large part of the discussion about matching parentheses is mostly hypothetical and using contrived examples. I don't know about you but I don't find myself having a hard time writing function calls in my day-to-day programming. If I get to a point where an expression gets hard to read or comprehend, I divide it into multiple expressions using intermediary variables. I don't see why try()
with function call syntax would be any different in this respect in practice.
@eandre Normally, functions do not have such a dynamic definition. Many forms of this proposal decrease safety surrounding the communication of control flow, and that's troublesome.
@networkimprov @daved I don't dislike these two ideas, but they don't feel like enough of an improvement over simply allowing single-line if err != nil { ... }
statements to warrant a language change. Also, does it do anything to reduce repetitive boilerplate in the case where you're simply returning the error? Or is the idea that you always have to write out the return
?
@brynbellomy In my example, there is no return
. relay
is a protective relay defined as "if this err is not nil, the following will be returned".
Using my second example from earlier:
f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)
Could also be something like:
f, err := os.Open(path)
relay(err)
With the error that trips the relay being returned along with zero values for other return values (or whatever values are set for named returned values). Another form that might be useful:
wrap := func(err error, msg string) error {
if err != nil {
fmt.Errorf("%s: %s", msg, err)
}
return nil
}
// ...
f, err := os.Open(path)
relay(err, wrap(err, fmt.Sprintf("Cannot open %s", path)))
Where the second relay arg is not called unless the relay is tripped by the first relay arg. The optional second relay error arg would be the value returned.
Should _go fmt_ allow single-line if
but not case, for, else, var ()
? I'd like them all, please ;-)
The Go team has turned aside many requests for single-line error checks.
on err, return err
statements could be repetitive, but they're explicit, terse, and clear.
@magical You're feedback has been addressed in the updated version of the detailed proposal.
A small thing, but if try
is a keyword it could be recognized as a terminating statement so instead of
func f() error {
try(g())
return nil
}
you can just do
func f() error {
try g()
}
(try
-statement gets that for free, try
-operator would need special handling, I realize the above is not a great example: but it is minimal)
@jimmyfrasche try
could be recognized as a terminating statement even if it is not a keyword - we already do that with panic
, there's no extra special handling needed besides what we already do. But besides that point, try
is not a terminating statement, and trying to make it one artificially seems odd.
All valid points. I guess it could only reliably be considered as a terminating statement if it's the very last line of a function that only returns an error, like CopyFile
in the detailed proposal, or it's being used as try(err)
in an if
where it's known that err != nil
. Doesn't seem worth it.
Since this thread is getting long and hard to follow (and starts to repeat itself to a degree), I think we all would agree we would need to compromise on "some of the upsides any proposal offers.
As we keep liking or disliking the proposed code permutations above, we are not helping ourselves getting a real sense of "is this a more sensible compromise than another/whats already been offered" ?
I think we need some objective criteria rate our "try" variations and alt-proposals.
We can of course also set some ground rules for no-go's (no backward compatibility would be one) , and leave a grey area for "does it look appealing/gut feeling etc (the "hard" criteria above can also be debatable...).
If we test any proposal against this list, and rate each point (boilerplate 5 point , readability 4 points etc), then instead I think we can align on:
Our options are probably A,B and C, moreover, someone wishing to add a new proposal, could test (to a degree) if his proposal meets the criteria.
If this makes sense, thumb this up, we can try to go over the original proposal
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md
And perhaps some of the other proposals inline the comments or linked, perhaps we would learn something, or even come up with a mix that would rate higher.
Criteria += reuse of error handling code, across package & within function
Thanks everybody for the continued feedback on this proposal.
The discussion has veered off a bit from the core issue. It also has become dominated by a dozen or so contributors (you know who you are) hashing out what amounts to alternative proposals.
So let me just put out a friendly reminder that this issue is about a _specific_ proposal. This is _not_ a solicitation of novel syntactic ideas for error handling (which is a fine thing to do, but it's not _this_ issue).
Let's get the discussion more focussed again and back on track.
Feedback is most productive if it helps identifying technical _facts_ that we missed, such as "this proposal doesn't work right in this case" or "it will have this implication that we didn't realize".
For instance, @magical pointed out that the proposal as written wasn't as extensible as claimed (the original text would have made it impossible to add a future 2nd argument). Luckily this was a minor problem that was easily addressed with a small adjustment to the proposal. His input directly helped making the proposal better.
@crawshaw took the time to analyze a couple hundred use cases from the std library and showed that try
rarely ends up inside another expression, thus directly refuting the concern that try
might become buried and invisible. That is very useful fact-based feedback, in this case validating the design.
In contrast, personal _aesthetic_ judgements are not very helpful. We can register that feedback, but we can't act upon it (besides coming up with another proposal).
Regarding coming up with alternative proposals: The current proposal is the fruit of a lot of work, starting with last year's draft design. We have iterated on that design multiple times and solicited feedback from many people before we felt comfortable enough to post it and recommending advancing it to the actual experiment phase, but we haven't done the experiment yet. It does make sense to go back to the drawing board if the experiment fails, or if feedback tells us in advance that it will clearly fail. If we redesign on the fly, based on first impressions, we're just wasting everybody's time, and worse, learn nothing in the process.
All that said, the most significant concern voiced by many with this proposal is that it doesn't explicitly encourage error decoration besides what we can do already in the language. Thank you, we have registered that feedback. We have received the very same feedback internally, before posting this proposal. But none of the alternatives we have considered are better than what we have now (and we have looked a many in depth). Instead we have decided to propose a minimal idea which addresses one part of error handling well, and which can be extended if need be, exactly to address this concern (the proposal talks about this at length).
Thanks.
(I note that a couple of people advocating for alternative proposals have started their own separate issues. That is a fine thing to do and helps keeping the respective issues focussed. Thanks.)
@griesemer
I totally agree we should focus and that's exactly what brought me to write:
If this makes sense, thumb this up, we can try to go over the original proposal
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md
Two questions:
LMKWYT.
Is using try()
with zero args (or a different builtin) still up for consideration or has this been ruled out.
After the changes to the proposal I'm still concerned how it makes the use of named return values more "common". However I don't have data to back that up :upside_down_face:.
If try()
with zero args (or a different builtin) is added to the proposal, could the examples in the proposal be updated to use try()
(or a different builtin) to avoid named returns?
@guybrand Upvoting and down-voting is a fine thing to express _sentiment_ - but that is about it. There is no more information in there. We are not going to make a decision based on vote count, i.e. sentiment alone. Of course, if everybody - say 90%+ - hates a proposal, that is probably a bad sign and we should think twice before moving ahead. But that does not appear to be the case here. A good number of people seem to be happy with try-ing things out, and have moved on to other things (and don't bother to comment on this thread).
As I tried to express above, sentiment at this stage of the proposal is not based on any actual experience with the feature; it's a feeling. Feelings tend to change over time, especially when one had a chance to actually experience the subject the feelings are about... :-)
@Goodwine Nobody has ruled out try()
to get to the error value; though _if_ something like this is needed, it may be better to have a predeclared err
variable as @rogpeppe suggested (I think).
Again, this proposal doesn't rule any of this out. Let's go there if we find out it is necessary.
@griesemer
I think you totally misunderstood me.
I'm not looking into up voting/down voting this or any proposal, I was just looking into a way to get a good sense of "Do we think it makes sense to take a decision based on hard criteria rather than 'I like x' or 'y doesnt look nice' "
From what you wrote - thats EXACTLY what you think... so please upvote my comment as in saying:
"I think we should set up a list of what this proposal aims to improve, and based on that we can
A. decide if that's meaningful enough
B. decide if it looks like proposal really solves what it aims to solve
C. (as you added) make the extra effort trying to see if its feasible...
@guybrand they're evidently convinced it's worth prototyping in pre-release 1.14(?) and collecting feedback from hands-on users. IOW a decision has been made.
Also, filed #32611 for discussion of on err, <statement>
@guybrand My apologies. Yes, I agree we need to look at the various properties of a proposal, such as boilerplate reduction, does it solve the problem at hand, etc. But a proposal is more than the sum of its parts - at the end of the day we need to look at the overall picture. This is engineering, and engineering is messy: There are many factors that play into a design, and even if objectively (based on hard criteria) a part of a design is not satisfactory, it may still be the "right" design overall. So I am little hesitant to support a decision based on some sort of _independent_ rating of the individual aspects of a proposal.
(Hopefully this addresses better what you meant.)
But regarding the relevant criteria, I believe this proposal makes it clear what it tries to address. That is, the list you are referring to already exists:
..., our goal is to make error handling more lightweight by reducing the amount of source code dedicated solely to error checking. We also want to make it more convenient to write error handling code, to raise the likelihood programmers will take the time to do it. At the same time we do want to keep error handling code explicitly visible in the program text.
It just so happens that for error decoration we suggest to use a defer
and named result parameters (or ye olde if
statement) because that doesn't need a language change - which is a fantastic thing because language changes have enormous hidden costs. We do get that plenty of commenters feel that this part of the design "totally sucks". Still, at this point, in the overall picture, with all we know, we think it may be good enough. On the other hand, we need a language change - language support, rather - to get rid of the boilerplate, and try
is about as minimal a change we could come up with. And clearly, everything is still explicit in the code.
I'd say that the reasons that there are so many reactions and so many mini-proposals is that this is an issue where almost everyone agrees that Go language does need to do something to lessen the boilerplate of error handling, but we do not really agree on how to do it.
This proposal, in essence boils down to a built in "macro" for a very common, yet specific case of boilerplate, much like the built in append()
function. So while it is useful for the particular id err!=nil { return err }
particular use case, that's also all it does. Since it isn't very helpful in other cases, nor really generally applicable, I'd say it's underwhelming. I have the feeling that most Go programmers where expecting a bit more, and so the discussion in this thread keeps on going.
It is counter intuitive as a function. Because it is not possible in Go to have function with this order of arguments func(... interface{}, error)
.
Typed first then variable number of anything pattern is everywhere in Go modules.
The more I think, I like the current proposal, as is.
If we need error handling, we always have the if statement.
Hi everyone. Thank you for the calm, respectful, constructive discussion so far. I spent some time taking notes and eventually got frustrated enough that I built a program to help me maintain a different view of this comment thread that should be more navigable and complete than what GitHub shows. (It also loads faster!) See https://swtch.com/try.html. I will keep it updated but in batches, not minute-by-minute. (This is a discussion that requires careful thought and is not helped by "internet time".)
I have some thoughts to add, but that will probably have to wait until Monday. Thanks again.
@mishak87 We address this in the detailed proposal. Note that we have other built-ins (try
, make
, unsafe.Offsetof
, etc.) that are "irregular" - that's what built-ins are for.
@rsc, super useful! If you're still revising it, maybe linkify the #id issue refs? And font-style sans-serif?
This probably has been covered before so I apologize for adding even more noise but just wanted to make a point about try builtin vs the try ... else idea.
I think try builtin function can be a bit frustrating during development. We might occasionally want to add debug symbols or add more error specific context before returning. One would have to re-write a line like
user := try(getUser(userID))
to
user, err := getUser(userID)
if err != nil {
// inspect error here
return err
}
Adding a defer statement can help but it's still not the best experience when a function throws multiple errors as it would trigger for every try() call.
Re-writing multiple nested try() calls in the same function would be even more annoying.
On the other hand, adding context or inspection code to
user := try getUser(userID)
would be as simple as adding a catch statement at end followed by the code
user := try getUser(userID) catch {
// inspect error here
}
Removing or temporarily disabling a handler would be as simple as breaking the line before catch and commenting it out.
Switching between try()
and if err != nil
feels a lot more annoying IMO.
This also applies to adding or removing error context. One can write try func()
while prototyping something very quickly and then add context to specific errors as needed as the program matures as opposed to try()
as a built-in where one would have to re-write the lines to add context or add extra inspection code during debugging.
I'm sure try() would be useful but as I imagine using it in my day to day work, I can't help but imagine how try ... catch
would be so much more helpful and much less annoying when I'd need to add/remove extra code specific to some errors.
Also, I feel that adding try()
and then recommending to use if err != nil
to add context is very similar to having make()
vs new()
vs :=
vs var
. These features are useful in different scenarios but wouldn't it be nice if we had less ways or even a single way to initialize variables? Of course no one is forcing anyone to use try and people can continue to use if err != nil but I feel this will split error handling in Go just like the multiple ways to assign new variables. I think whatever method is added to the language should also provide a way to easily add/remove error handlers instead of forcing people to rewrite entire lines to add/remove handlers. That doesn't feel like a good outcome to me.
Sorry again for the noise but wanted to point it out in case someone wanted to write a separate detailed proposal for the try ... else
idea.
//cc @brynbellomy
Thanks, @owais, for bringing this up again - it's a fair point (and the debugging issue has indeed been mentioned before). try
does leave the door open for extensions, such as a 2nd argument, which could be a handler function. But it is true that a try
function doesn't make debugging easier - one may have to rewrite the code a bit more than a try
-catch
or try
- else
.
@owais
Adding a defer statement can help but it's still not the best experience when a function throws multiple errors as it would trigger for every try() call.
You could always include a type switch in the deferred function which would handle (or not) different types of error in an appropriate way before returning.
Given the discussion thus far — specifically the responses from the Go team — I am getting the strong impression the team plans to move forward with the proposal that is on the table. If yes, then a comment and a request:
The as-is proposal IMO will result in a non-insignificant reduction in code quality in the publicly available repos. My expectation is many developers will take the path of least resistance, effectively use exception handling techniques and choose to use try()
instead of handling errors at the point they occur. But given the prevailing sentiment on this thread I realize that any grandstanding now would just be fighting a losing battle so I am just registering my objection for posterity.
Assuming that the team does move forward with the proposal as currently written, can you please add a compiler switch that will disable try()
for those who do not want any code that ignores errors in this manner and to disallow programmers they hire from using it? _(via CI, of course.)_ Thank you in advance for this consideration.
can you please add a compiler switch that will disable try()
This would have to be on a linting tool, not on the compiler IMO, but I agree
This would have to be on a linting tool, not on the compiler IMO, but I agree
I am explicitly requesting a compiler option and not a linting tool because to disallow compiling such as option. Otherwise it will be too easy to _"forget"_ to lint during local development.
@mikeschinkel Wouldn't it be just as easy to forget to turn on the compiler option in that situation?
Compiler flags should not change the spec of the language. This is much more fit for vet/lint
Wouldn't it be just as easy to forget to turn on the compiler option in that situation?
Not when using tools like GoLand where there is no way to force a lint to be run before a compile.
Compiler flags should not change the spec of the language.
-nolocalimports
changes the spec, and -s
warns.
Compiler flags should not change the spec of the language.
-nolocalimports
changes the spec, and-s
warns.
No, it doesn't change the spec. Not only does the grammar of the language continue to remain the same, but the spec specifically states:
The interpretation of the ImportPath is implementation-dependent but it is typically a substring of the full file name of the compiled package and may be relative to a repository of installed packages.
Not when using tools like GoLand where there is no way to force a lint to be run before a compile.
https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint
@deanveloper
https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint
Certainly that exists, but you are comparing apple-to-organges. What you are showing is a file watcher that runs on files changing and since GoLand autosaves files that means it runs constantly which generates far more noise than signal.
The lint always does not and cannot (AFAIK) be configured to as a pre-condition for running the compiler:
No, it doesn't change the spec. Not only does the grammar of the language continue to remain the same, but the spec specifically states:
You are playing with semantics here instead of focusing on the outcome. So I will do the same.
I request that a compiler option be added that will disallow compiling code with try()
. That is not a request to change the language spec, it is just a request to for the compiler to halt in this special case.
And if it helps, the language spec can be updated to say something like:
The interpretation of
try()
is implementation-dependent but it is typically a one that triggers a return when the last parameter is an error however it can be implemented to not be allowed.
The time to ask for a compiler switch or vet check is after the try()
prototype lands in 1.14(?) tip. At that point you'd file a new issue for it (and yes I think it's a good idea). We've been asked to restrict comments here to factual input about the current design doc.
Hi so just to add on to the whole issue with adding debug statements and such during development.
I think that the second parameter idea is fine for the try()
function, but another idea just to throw it out there is by adding an emit
clause to be a second part for try()
.
For instance, I believe when developing and such there could be a case when I want to call fmt
for this instant to print the error. So I could go from this:
func writeStuff(filename string) (io.ReadCloser, error) {
f := try(os.Open(filename))
try(fmt.Fprintf(f, "stuff\n"))
return f, nil
}
Can be re-written to something like this for debug statements or general handling or the error before returning.
func writeStuff(filename string) (io.ReadCloser, error) {
emit err {
fmt.Printf("something happened [%v]\n", err.Error())
return nil, err
}
f := try(os.Open(filename))
try(fmt.Fprintf(f, "stuff\n"))
return f, nil
}
So here I did end up putting a proposal for a new keyword emit
which could be a statement or a one liner for immediate returning like the initial try()
functionality:
emit return nil, err
What the emit would be is essentially just a clause where you can put any logic you wish in it if the try()
gets triggered by an error not equalling nil. Another ability with the emit
keyword is that you're able to access the error right there if you add just after the keyword a variable name such as I did in the first example using it.
This proposal does create a little verbosity to the try()
function, but I think it's at least a little more clear on what is happening with the error. This way you're able to decorate the errors too without having it jammed all into one line and you can see how the errors are handles immediately when you're reading the function.
This is a response to @mikeschinkel, I'm putting my response in a detail block so that I don't clutter up the discussion too much. Either way, @networkimprov is correct that this discussion should be tabled until after this proposal gets implemented (if it does).
details about a flag to disable try
@mikeschinkel
The lint always does not and cannot (AFAIK) be configured to as a pre-condition for running the compiler:
Reinstalled GoLand just to test this. This seems to work just fine, the only difference being is that if the lint finds something it doesn't like, it doesn't fail the compilation. That could easily be fixed with a custom script though, that runs golint
and fails with a nonzero exit code if there is any output.
(Edit: I fixed the error that it was trying to tell me at the bottom. It was running fine even while the error was present, but changing "Run Kind" to directory removed the error and it worked fine)
Also another reason why it should NOT be a compiler flag - all Go code is compiled from source. That includes libraries. That means that if you want to turn of try
through the compiler, you'd be turning off try
for every single one of the libraries you are using as well. It's just a bad idea to have it be a compiler flag.
You are playing with semantics here instead of focusing on the outcome.
No, I am not. Compiler flags should not change the spec of the language. The spec is very well layed-out and in order for something to be "Go", it needs to follow the spec. The compiler flags you have mentioned do change the behavior of the language, but no matter what, they make sure the language still follows the spec. This is an important aspect of Go. As long as you follow the Go spec, your code should compile on any Go compiler.
I request that a compiler option be added that will disallow compiling code with try(). That is not a request to change the language spec, it is just a request to for the compiler to halt in this special case.
It is a request to change the spec. This proposal in itself is a request to change the spec. Builtin functions are very specifically included in the spec.. Asking to have a compiler flag that removes the try
builtin would therefore be a compiler flag that would change the spec of the language being compiled.
That being said, I think that ImportPath
should be standardized in the spec. I may make a proposal for this.
And if it helps, the language spec can be updated to say something like [...]
While this is true, you would not want the implementation of try
to be implementation dependent. It's made to be an important part of the language's error handling, which is something that would need to be the same across every Go compiler.
@deanveloper
_"Either way, @networkimprov is correct that this discussion should be tabled until after this proposal gets implemented (if it does)."_
Then why did you decide to ignore that suggestion and post in this thread anyway instead of waiting for later? You argued your points here while at the same time asserting that I should not challenge your points. Practice what you preach...
Given you choice, I will choose to respond too, also in a detail block here:
_"That could easily be fixed with a custom script though, that runs golint and fails with a nonzero exit code if there is any output."_
Yes, with enough coding _any_ problem can be fixed. But we both know from experience that the more complex a solution is the fewer people who want to use it will actually end up using it.
So I was explicitly asking for a simple solution here, not a roll-your-own solution.
_"you'd be turning off try for every single one of the libraries you are using as well."_
And that is _explicitly_ the reason why I requested it. Because I want to ensure that all code that uses this troublesome _"feature"_ will not make its way into executables we distribute.
_"It is a request to change the spec. This proposal in itself is a request to change the spec._"
It is ABSOLUTELY not a change to the spec. It is a request for a switch to change the _behavior_ of the build
command, not a change in language spec.
If someone asks for the go
command to have a switch to display its terminal output in Mandarin, that is not a change to the language spec.
Similarly if go build
were to sees this switch then it would simply issue an error message and halt when it comes across a try()
. No language spec changes needed.
_"It's made to be an important part of the language's error handling, which is something that would need to be the same across every Go compiler."_
It will be a problematic part of the language's error handling and making it optional will allow those who want to avoid its problems to be able to do so.
Without the switch it is likely most people will just see as a new feature and embrace it and never ask themselves if in fact it should be used.
_With the switch_ — and articles explaining the new feature that mention the switch — many people will understand that it has problematic potential and thus will allow the Go team to study if it was a good inclusion or not by seeing how much public code avoids using it vs. how public code uses it. That could inform design of Go 3.
_"No, I am not. Compiler flags should not change the spec of the language."_
Saying you are not playing semantics does not mean you are not playing semantics.
Fine. Then I instead request a new top level command called _(something like)_ build-guard
used to disallow problematic features during compilation, starting with disallowing try()
.
Of course the best outcome is if the try()
feature is tabled with a plan to reconsider solving the issue a different way the future, a way in which the vast majority agrees with. But I fear the ship has already sailed on try()
so I am hoping to minimize its downside.
So now if you truly agree with @networkimprov then hold your reply until later, as they suggested.
Sorry to interrupt, but I have facts to report :-)
I'm sure the Go team has already benchmarked defer, but I haven't seen any numbers...
$ go test -bench=.
goos: linux
goarch: amd64
BenchmarkAlways2-2 20000000 72.3 ns/op
BenchmarkAlways4-2 20000000 68.1 ns/op
BenchmarkAlways6-2 20000000 68.0 ns/op
BenchmarkNever2-2 100000000 16.5 ns/op
BenchmarkNever4-2 100000000 13.1 ns/op
BenchmarkNever6-2 100000000 13.5 ns/op
Source
package deferbench
import (
"fmt"
"errors"
"testing"
)
func Always(iM, iN int) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("d: %v", err)
}
}()
if iN % iM == 0 {
return errors.New("e")
}
return nil
}
func Never(iM, iN int) (err error) {
if iN % iM == 0 {
return fmt.Errorf("r: %v", errors.New("e"))
}
return nil
}
func BenchmarkAlways2(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e2, a) }}
func BenchmarkAlways4(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e4, a) }}
func BenchmarkAlways6(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e6, a) }}
func BenchmarkNever2(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e2, a) }}
func BenchmarkNever4(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e4, a) }}
func BenchmarkNever6(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e6, a) }}
@networkimprov
From https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md#efficiency-of-defer (my emphasis in bold)
Independently, the Go runtime and compiler team has been discussing alternative implementation options and we believe that we can make typical defer uses for error handling about as efficient as existing “manual” code. We hope to make this faster defer implementation available in Go 1.14 (see also * CL 171758 * which is a first step in this direction).
i.e. defer is now 30% performance improvement for go1.13 for common usage, and should be faster and just as efficient as non-defer mode in go 1.14
Maybe someone can post numbers for 1.13 and the 1.14 CL?
Optimizations don't always survive contact with the enemy... er, ecosystem.
1.13 defers will be about 30% faster:
name old time/op new time/op delta
Defer-4 52.2ns ± 5% 36.2ns ± 3% -30.70% (p=0.000 n=10+10)
This is what I get on @networkimprov 's tests above (1.12.5 to tip):
name old time/op new time/op delta
Always2-4 59.8ns ± 1% 47.5ns ± 1% -20.57% (p=0.008 n=5+5)
Always4-4 57.9ns ± 2% 43.5ns ± 1% -24.96% (p=0.008 n=5+5)
Always6-4 57.6ns ± 2% 44.1ns ± 1% -23.43% (p=0.008 n=5+5)
Never2-4 13.7ns ± 8% 3.8ns ± 4% -72.27% (p=0.008 n=5+5)
Never4-4 10.5ns ± 6% 1.3ns ± 2% -87.76% (p=0.008 n=5+5)
Never6-4 10.8ns ± 6% 1.2ns ± 1% -88.46% (p=0.008 n=5+5)
(I'm not sure why Never ones are so much faster. Maybe inlining changes?)
The optimizations for defers for 1.14 are not implemented yet, so we don't know what the performance will be. But we think we should get close to the performance of a regular function call.
Then why did you decide to ignore that suggestion and post in this thread anyway instead of waiting for later?
The details block was edited in later, after I had read @networkimprov's comment. I'm sorry for making it look like I had understood what he said and ignored it. I'm ending the discussion after this statement, I wanted to explain myself since you had asked me why I posted the comment.
Regarding the optimizations to defer, I'm excited for them. They help this proposal a little bit, making defer HandleErrorf(...)
a bit less heavy. I still don't like the idea of abusing named parameters in order for this trick to work, though. How much is it expected to speed up by for 1.14? Should they run at similar speeds?
@griesemer One area that might be worth expanding a bit more is how transitions work in a world with try
, perhaps including:
vet
or staticcheck
or similar, vs. (c) might lead to a bug that might not be noticed or would need to be caught via testing.gopls
(or another utility) could or should have a role in automating common decoration style transitions.This is not exhaustive, but a representative set of stages could be something like:
0. No error decoration (e.g., using try
without any decoration).
1. Uniform error decoration (e.g., using try
+ defer
for uniform decoration).
2. N-1 exit points have uniform error decoration, but 1 exit point has different decoration (e.g., perhaps a permanent detailed error decoration in just one location, or perhaps a temporary debug log, etc.).
3. All exit points each have unique error decoration, or something approaching unique.
Any given function is not going to have a strict progression through those stages, so maybe "stages" is the wrong word, but some functions will transition from one decoration style to another, and it could be useful to be more explicit about what those transitions are like when or if they happen.
Stage 0 and stage 1 seem to be sweet spots for the current proposal, and also happen to be fairly common use cases. A stage 0->1 transition seems straightforward. If you were using try
without any decoration in stage 0, you can add something like defer fmt.HandleErrorf(&err, "foo failed with %s", arg1)
. You might at that moment also need to introduce named return parameters under the proposal as initially written. However, if the proposal adopts one of the suggestions along the lines of a predefined built-in variable that is an alias for the final error result parameter, then the cost and risk of error here might be small?
On the other hand, a stage 1->2 transition seems awkward (or "annoying" as some others have said) if stage 1 was uniform error decoration with a defer
. To add one specific bit of decoration at one exit point, first you would need to remove the defer
(to avoid double decoration), then it seems one would need to visit all the return points to desugar the try
uses into if
statements, with N-1 of the errors getting decorated the same way and 1 getting decorated differently.
A stage 1->3 transition also seems awkward if done manually.
Some mistakes that might happen as part of a manual desugaring process include accidentally shadowing a variable, or changing how a named return parameter is affected, etc. For example, if you look at the first and largest example in the "Examples" section of the try proposal, the CopyFile
function has 4 try
uses, including in this section:
w := try(os.Create(dst))
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
If someone did an "obvious" manual desugaring of w := try(os.Create(dst))
, that one line could be expanded to:
w, err := os.Create(dst)
if err != nil {
// do something here
return err
}
That looks good at first glance, but depending on what block that change is in, that could also accidentally shadow the named return parameter err
and break the error handling in the subsequent defer
.
To help with the time cost and risk of mistakes, perhaps gopls
(or another utility) could have some type of command to desugar a specific try
, or a command desugar all uses of try
in a given func that could be mistake-free 100% of the time. One approach might be any gopls
commands only focus on removing and replacing try
, but perhaps a different command could desugar all uses of try
while also transforming at least common cases of things like defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
at the top of the function into the equivalent code at each of the former try
locations (which would help when transitioning from stage 1->2 or stage 1->3). That is not a fully baked idea, but perhaps worth more thought as to what is possible or desirable or updating the proposal with current thinking.
A related comment is it is not immediately obvious is how frequently a programmatic mistake free transformation of a try
would end up looking like normal idiomatic Go code. Adapting one of the examples from the proposal, if for example you wanted to desugar:
x1, x2, x3 = try(f())
In some cases, a programmatic transform that preserves behavior could end up with something like:
t1, t2, t3, te := f() // visible temporaries
if te != nil {
return x1, x2, x3, te
}
x1, x2, x3 = t1, t2, t3
That exact form might be rare, and it seems the results of an editor or IDE doing programatic desugaring could often end up looking more idiomatic, but it would be interesting to hear how true that is, including in the face of named return parameters possibly becoming more common, and taking into account shadowing, :=
vs =
, other uses of err
in the same function, etc.
The proposal talks about possible behavior differences between if
and try
due to named result parameters, but in that particular section it seems to be talking mainly about transitioning from if
to try
(in the section that concludes _"While this is a subtle difference, we believe cases like these are rare. If current behavior is expected, keep the if statement."_). In contrast, there might be different possible mistakes worth elaborating when transitioning from try
back to if
while preserving identical behavior.
In any event, sorry for the long comment, but it seems a fear of high transition costs between styles is underlying some of the concern expressed in some of the other comments posted here, and hence the suggestion to be more explicit about those transition costs and potential mitigations.
@thepudds I love you are highlighting the costs and potential bugs associated with how language features can either positive or negatively affect refactoring. It is not a topic I see often discussed, but one that can have a large downstream effect.
a stage 1->2 transition seems awkward if stage 1 was uniform error decoration with a defer. To add one specific bit of decoration at one exit point, first you would need to remove the defer (to avoid double decoration), then it seems one would need to visit all the return points to desugar the try uses into if statements, with N-1 of the errors getting decorated the same way and 1 getting decorated differently.
This is where using break
instead of return
shines with 1.12. Use it in a for range once { ... }
block where once = "1"
to demarcate the sequence of code that you might want to exit from and then if you need to decoration just one error you do it at the point of break
. And if you need to decorate all errors you do it just before the sole return
at the end of the method.
The reason it is such a good pattern is it is resilient to changing requirements; you rarely ever have to break working code to implement new requirements. And it is a cleaner and more obvious approach IMO than jumping back to the beginning of the method before then jumping out of it.
@randall77's results for my benchmark show a 40+ns per call overhead for both 1.12 & tip. That implies that defer can inhibit optimizations, rendering improvements to defer moot in some cases.
@networkimprov Defer does currently inhibit optimizations, and that's part of what we'd like to fix. For instance, it would be nice to inline the body of the defer'd function just like we inline regular calls.
I fail to see how any improvements we make would be moot. Where does that assertion come from?
Where does that assertion come from?
The 40+ns per call overhead for a function with a defer to wrap the error didn't change.
The changes in 1.13 are one part of optimizing defer. There are other improvements planned. This is covered in the design document, and in the part of the design document quoted at some point above.
Re swtch.com/try.html and https://github.com/golang/go/issues/32437#issuecomment-502192315:
@rsc, super useful! If you're still revising it, maybe linkify the #id issue refs? And font-style sans-serif?
That page is about content. Don't focus on the rendering details. I'm using the output of blackfriday on the input markdown unaltered (so no GitHub-specific #id links), and I am happy with the serif font.
I'm sorry, but there will not be compiler options to disable specific Go features, nor will there be vet checks saying not to use those features. If the feature is bad enough to disable or vet, we will not put it in. Conversely, if the feature is there, it is OK to use. There is one Go language, not a different language for each developer based on their choice of compiler flags.
@mikeschinkel, twice now on this issue you have described the use of try as _ignoring_ errors.
On June 7 you wrote, under the heading "Makes it easier for developers to ignore errors":
This is a total repeat of what others have comments, but what basically providing
try()
is analogous in many ways to simply embracing the following as idomatic code, and this is code that will never find its way into any code any self-respecting developer ships:f, _ := os.Open(filename)
I know I can be better in my own code, but I also know many of us depend on the largess of other Go developers who publish some tremendously useful packages, but from what I have seen in _"Other People's Code(tm)"_ best practices in error handling is often ignored.
So seriously, do we really want to make it easier for developers to ignore errors and allow them to polute GitHub with non-robust packages?
And then on June 14 again you referred to using try as "code that ignores errors in this manner".
If not for the code snippet f, _ := os.Open(filename)
, I would think you were simply exaggerating by characterizing "checking for an error and returning it" as "ignoring" an error. But the code snippet, along with the many questions already answered in the proposal document or in the language spec make me wonder whether we are talking about the same semantics after all. So just to be clear and answer your questions:
When studying the proposal's code I find that the behaviour is non-obvious and somewhat hard to reason about.
When I see
try()
wrapping an expression, what will happen if an error is returned?
When you see try(f())
, if f()
returns an error, the try
will stop execution of the code and return that error from the function in whose body the try
appears.
Will the error just be ignored?
No. The error is never ignored. It is returned, the same as using a return statement. Like:
{ err := f(); if err != nil { return err } }
Or will it jump to the first or the most recent
defer
,
The semantics are the same as using a return statement.
Deferred functions run in "in the reverse order they were deferred."
and if so will it automatically set a variable named
err
inside the closure that, or will it pass it as a parameter _(I don't see a parameter?)_.
The semantics are the same as using a return statement.
If you need to refer to a result parameter in a deferred function body, you can give it a name. See the result
example in https://golang.org/ref/spec#Defer_statements.
And if not an automatic error name, how do I name it? And does that mean I can't declare my own
err
variable in my function, to avoid clashes?
The semantics are the same as using a return statement.
A return statement always assigns to the actual function results, even if the result is unnamed, and even if the result is named but shadowed.
And will it call all
defer
s? In reverse order or regular order?
The semantics are the same as using a return statement.
Deferred functions run in "in the reverse order they were deferred." (Reverse order is regular order.)
Or will it return from both the closure and the
func
where the error was returned? _(Something I would never have considered if I had not read here words that imply that.)_
I don't know what this means but probably the answer is no. I would encourage focusing on the proposal text and the spec and not on other commentary here about what that text might or might not mean.
After reading the proposal and all the comments thus far I still honestly do not know the answers to the above questions. Is that the kind of feature we want to add to a language whose advocates champion as being _"Captain Obvious?"_
In general we do aim for a simple, easy-to-understand language. I am sorry you had so many questions. But this proposal really is reusing as much of the existing language as possible (in particular, defers), so there should be very few additional details to learn. Once you know that
x, y := try(f())
means
tmp1, tmp2, tmpE := f()
if tmpE != nil {
return ..., tmpE
}
x, y := tmp1, tmp2
almost everything else should follow from the implications of that definition.
This is not "ignoring" errors. Ignoring an error is when you write:
c, _ := net.Dial("tcp", "127.0.0.1:1234")
io.Copy(os.Stdout, c)
and the code panics because net.Dial failed and the error was ignored, c is nil, and io.Copy's call to c.Read faults. In contrast, this code checks and returns the error:
c := try(net.Dial("tcp", "127.0.0.1:1234"))
io.Copy(os.Stdout, c)
To answer your question about whether we want to encourage the latter over the former: yes.
@damienfamed75 Your proposedemit
statement looks essentially the same as the handle
statement of the draft design. The primary reason for abandoning the handle
declaration was its overlap with defer
. It's not clear to me why one couldn't just use a defer
to get the same effect that emit
achieves.
@dominikh asked:
Will acme start highlighting try?
So much about the try proposal is undecided, up in the air, unknown.
But this question I can answer definitively: no.
@rsc
Thank you for your response.
_"twice now on this issue you have described the use of try as ignoring errors."_
Yes, I was commenting using my perspective and not being technically correct.
What I meant was _"Allowing errors to be passed on without being decorated."_ To me that is _"ignoring"_ — much like how people using exception handling _"ignore"_ errors — but I can certainly see how others would view my wording as not being technically correct.
_"When you see
try(f())
, iff()
returns an error, the try will stop execution of the code and return that error from the function in whose body the try appears."_
That was an answer to a question from my comment a while back, but by now I have figured that out.
And it ends up doing two things that make me sad. Reasons:
It will make the path of least resistance to avoid decorating errors — encouraging lots of developers to do just that — and many will publish that code for others to use resulting in more lower-quality publicly-available code with less robust error handling/error reporting.
For those like me who use break
and continue
for error handling instead of return
— a pattern that is more resilient to changing requirements — we won't even be able to use try()
, even when there really is no reason to annotate the error.
_"Or will it return from both the closure and the func where the error was returned? (Something I would never have considered if I had not read here words that imply that.)"_
_"I don't know what this means but probably the answer is no. I would encourage focusing on the proposal text and the spec and not on other commentary here about what that text might or might not mean."_
Again, that question was over a week ago so I have a better understand now.
To clarify, for posterity, the defer
has a closure, right? If you return from that closure then — unless I misunderstand — it will not only return from the closure but also return from the func
where the error occurred, right? _(No need to reply if yes.)_
func example() {
defer func(err) {
return err // returns from both defer and example()
}
try(SomethingThatReturnsAnError)
}
BTW, my understanding is the reason for try()
is because developers have complained about boilerplate. I also find that sad because I think that the requirement to accept returned errors that results in this boilerplate is what helps make Go apps more robust than in many other languages.
I personally would prefer to see you make it harder to not decorate errors than to make it easier to ignore decorating them. But I do acknowledge that I appear to be in the minority on this.
BTW, some people have proposed syntax like one of the following _(I have added a hypothetical .Extend()
to keep my examples concise):_
f := try os.Open(filename) else err {
err.Extend("Cannot open file %s",filename)
//break, continue or return err
}
Or
try f := os.Open(filename) else err {
err.Extend("Cannot open file %s",filename)
//break, continue or return err
}
And then others claim that it does not really save any characters over this:
f,err := os.Open(filename)
if err != nil {
err.Extend("Cannot open file %s",filename)
//break, continue or return err
}
But one thing that criticism is missing that it moves from 5 lines to 4 lines, a reduction of vertical space and that seems significant, especially when you need many such constructs in a func
.
Even better would be something like this which would eliminate 40% of vertical space _(though given the comments about keywords I doubt this would be considered):_
try f := os.Open(filename)
else err().Extend("Cannot open file %s",filename)
end //break, continue or return err
#fwiw
ANYWAY, like I said earlier, I guess the ship has sailed so I will just learn to accept it.
Goals
A few comments here have questioned what it is we are trying to do with the proposal. As a reminder, the Error Handling Problem Statement we published last August says in the “Goals” section:
“For Go 2, we would like to make error checks more lightweight, reducing the amount of Go program text dedicated to error checking. We also want to make it more convenient to write error handling, raising the likelihood that programmers will take the time to do it.
Both error checks and error handling must remain explicit, meaning visible in the program text. We do not want to repeat the pitfalls of exception handling.
Existing code must keep working and remain as valid as it is today. Any changes must interoperate with existing code.”
For more about “the pitfalls of exception handling,” see the discussion in the longer “Problem” section. In particular, the error checks must be clearly attached to what is being checked.
@mikeschinkel,
To clarify, for posterity, the
defer
has a closure, right? If you return from that closure then — unless I misunderstand — it will not only return from the closure but also return from thefunc
where the error occurred, right? _(No need to reply if yes.)_
No. This is not about error handling but about deferred functions. They are not always closures. For example, a common pattern is:
func (d *Data) Op() int {
d.mu.Lock()
defer d.mu.Unlock()
... code to implement Op ...
}
Any return from d.Op runs the deferred unlock call after the return statement but before code transfers to the caller of d.Op. Nothing done inside d.mu.Unlock affects the return value of d.Op. A return statement in d.mu.Unlock returns from the Unlock. It does not by itself return from d.Op. Of course, once d.mu.Unlock returns, so does d.Op, but not directly because of d.mu.Unlock. It's a subtle point but an important one.
Getting to your example:
func example() { defer func(err) { return err // returns from both defer and example() } try(SomethingThatReturnsAnError) }
At least as written, this is an invalid program. I am not trying to be pedantic here - the details matter. Here is a valid program:
func example() (err error) {
defer func() {
if err != nil {
println("FAILED:", err.Error())
}
}()
try(funcReturningError())
return nil
}
Any result from a deferred function call is discarded when the call is executed, so in the case where what is deferred is a call to a closure, it makes no sense at all to write the closure to return a value. So if you were to write return err
inside the closure body, the compiler will tell you "too many arguments to return".
So, no, writing return err
does not return from both the deferred function and the outer function in any real sense, and in conventional usage it's not even possible to write code that appears to do that.
Many of the counter-proposals posted to this issue suggesting other, more capable error-handling constructs duplicate existing language constructs, like the if statement. (Or they conflict with the goal of “making error checks more lightweight, reducing the amount of Go program text to error checking.” Or both.)
In general, Go already has a perfectly capable error-handling construct: the entire language, especially if statements. @DavexPro was right to refer back to the Go blog entry Errors are values. We need not design a whole separate sub-language concerned with errors, nor should we. I think the main insight over the past half year or so has been to remove “handle” from the “check/handle” proposal in favor of reusing what language we already have, including falling back to if statements where appropriate. This observation about doing as little as possible eliminates from consideration most of the ideas around further parameterizing a new construct.
With thanks to @brynbellomy for his many good comments, I will use his try-else as an illustrative example. Yes, we might write:
func doSomething() (int, error) {
// Inline error handler
a, b := try SomeFunc() else err {
return 0, errors.Wrap(err, "error in doSomething:")
}
// Named error handlers
handler logAndContinue err {
log.Errorf("non-critical error: %v", err)
}
handler annotateAndReturn err {
return 0, errors.Wrap(err, "error in doSomething:")
}
c, d := try SomeFunc() else logAndContinue
e, f := try OtherFunc() else annotateAndReturn
// ...
return 123, nil
}
but all things considered this is probably not a significant improvement over using existing language constructs:
func doSomething() (int, error) {
a, b, err := SomeFunc()
if err != nil {
return 0, errors.Wrap(err, "error in doSomething:")
}
// Named error handlers
logAndContinue := func(err error) {
log.Errorf("non-critical error: %v", err)
}
annotate:= func(err error) (int, error) {
return 0, errors.Wrap(err, "error in doSomething:")
}
c, d, err := SomeFunc()
if err != nil {
logAndContinue(err)
}
e, f, err := SomeFunc()
if err != nil {
return annotate(err)
}
// ...
return 123, nil
}
That is, continuing to rely on the existing language to write error handling logic seems preferable to creating a new statement, whether it's try-else, try-goto, try-arrow, or anything else.
This is why try
is limited to the simple semantics if err != nil { return ..., err }
and nothing more: shorten the one common pattern but don't try to reinvent all possible control flow. When an if statement or a helper function is appropriate, we fully expect people to continue to use them.
@rsc Thanks for clarifying.
Correct, I didn't get the details right. I guess I don't use defer
often enough to remember its syntax.
_(FWIW I find using defer
for anything more complex than closing a file handle less obvious because of the jumping backwards in the func
before returning. So always just put that code at the end of the func
after the for range once{...}
my error handling code break
s out of.)_
The suggestion to gofmt every try call into multiple lines directly conflicts with the goal of “making error checks more lightweight, reducing the amount of Go program text to error checking.”
The suggestion to gofmt an error-testing if statement in a single line also directly conflicts with this goal. The error checks do not become substantially more lightweight nor reduced in amount by removing the interior newline characters. If anything, they become more difficult to skim.
The main benefit of try is to have a clear abbreviation for the one most common case, making the unusual ones stand out more as worth reading carefully.
Backing up from gofmt to general tools, the suggestion to focus on tooling for writing error checks instead of a language change is equally problematic. As Abelson and Sussman put it, “Programs must be written for people to read, and only incidentally for machines to execute.” If machine tooling is _required_ to cope with the language, then the language is not doing its job. Readability must not be limited to people using specific tools.
A few people ran the logic in the opposite direction: people can write complex expressions, so they inevitably will, so you'd need IDE or other tool support to find the try expressions, so try is a bad idea. There are a few unsupported leaps here, though. The main one is the claim that because it is _possible_ to write complex, unreadable code, such code will become ubiquitous. As @josharian noted, it is already “possible to write abominable code in Go.” That's not commonplace because developers have norms about trying to find the most readable way to write a particular piece of code. So it is most certainly _not_ the case that IDE support will be required to read programs involving try. And in the few cases where people write truly terrible code abusing try, IDE support is unlikely to be much use. This objection—people can write very bad code using the new feature—is raised in pretty much every discussion of every new language feature in every language. It is not terribly helpful. A more helpful objection would be of the form “people will write code that seems good at first but turns out to be less good for this unexpected reason,” like in the discussion of debugging prints.
Again: Readability must not be limited to people using specific tools.
(I still print and read programs on paper, although people often give me weird looks for doing that.)
Thanks @rsc for providing your thoughts on allowing if
statements to be gofmt'd as a single line.
The suggestion to gofmt an error-testing if statement in a single line also directly conflicts with this goal. The error checks do not become substantially more lightweight nor reduced in amount by removing the interior newline characters. If anything, they become more difficult to skim.
I estimate these assertions differently.
I find reducing the number of lines from 3 to 1 to be substantially more lightweight. Wouldn't gofmt requiring an if statement to contain, for example, 9 (or even 5) newlines instead of 3 be substantially more heavyweight? It's the same factor (amount) of reduction/expansion. I'd argue that struct literals have this exact trade-off, and with the addition of try
, will allow control flow just as much as an if
statement.
Secondly, I find the argument that they become more difficult to skim to apply equally well to try
, if not more. At least an if
statement would have to be on it's own line. But perhaps I misunderstand what is meant by "skim" in this context. I'm using it to mean "mostly skip over but be aware of."
All that said, the gofmt suggestion was predicated on taking an even more conservative step than try
and has no impact on try
unless it would be sufficient. It sounds like it's not, and so if I want to discuss it more I'll open a new issue/proposal. :+1:
I find reducing the number of lines from 3 to 1 to be substantially more lightweight.
I think everyone agrees that it is possible for code to too dense. For example if your entire package is one line I think we all agree that's a problem. We all probably disagree on the precise line. For me, we've established
n, err := src.Read(buf)
if err == io.EOF {
return nil
} else if err != nil {
return err
}
as the way to format that code, and I think it would be quite jarring to try to shift to your example
n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }
instead. If we'd started out that way, I'm sure it would be fine. But we didn't, and it's not where we are now.
Personally, I do find the former lighter weight on the page in the sense that it is easier to skim. You can see the if-else at a glance without reading any actual letters. In contrast, the denser version is hard to tell at a glance from a sequence of three statements, meaning you have to look more carefully before its meaning becomes clear.
In the end, it's OK if we draw the denseness-vs-readability line in different places as far as number of newlines. The try proposal is focused on not just removing newlines but removing the constructs entirely, and that produces a lighter-weight page presence separate from the gofmt question.
A few people ran the logic in the opposite direction: people can write complex expressions, so they inevitably will, so you'd need IDE or other tool support to find the try expressions, so try is a bad idea. There are a few unsupported leaps here, though. The main one is the claim that because it is _possible_ to write complex, unreadable code, such code will become ubiquitous. As @josharian noted, it is already “possible to write abominable code in Go.” That's not commonplace because developers have norms about trying to find the most readable way to write a particular piece of code. So it is most certainly _not_ the case that IDE support will be required to read programs involving try. And in the few cases where people write truly terrible code abusing try, IDE support is unlikely to be much use. This objection—people can write very bad code using the new feature—is raised in pretty much every discussion of every new language feature in every language. It is not terribly helpful.
Isn't this the entire reason Go doesn't have a ternary operator?
Isn't this the entire reason Go doesn't have a ternary operator?
No. We can and should distinguish between "this feature can be used for writing very readable code, but may also be abused to write unreadable code" and "the dominant use of this feature will be to write unreadable code".
Experience with C suggests that ? : falls squarely into the second category. (With the possible exception of min and max, I'm not sure I've ever seen code using ? : that was not improved by rewriting it to use an if statement instead. But this paragraph is getting off topic.)
Syntax
This discussion has identified six different syntaxes to write the same semantics from the proposal:
f := try(os.Open(file))
, from the proposal (builtin function)f := try os.Open(file)
, using a keyword (prefix keyword)f := os.Open(file)?
, like in Rust (call-suffix operator)f := os.Open?(file)
, suggested by @rogpeppe (call-infix operator)try f := os.Open(file)
, suggested by @thepudds (try statement)try ( f := os.Open(file); f.Close() )
, suggested by @bakul (try block)(Apologies if I got the origin stories wrong!)
All of these have pros and cons, and the nice thing is that because they all have the same semantics, it is not too important to choose between the various syntaxes in order to experiment further.
I found this example by @brynbellomy thought-provoking:
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))
// vs
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)
// vs
try (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
)
There is not much difference between these specific examples, of course. And if the try is there in all the lines, why not line them up or factor them out? Isn't that cleaner? I wondered about this too.
But as @ianlancetaylor observed, “the try buries the lede. Code becomes a series of try statements, which obscures what the code is actually doing.”
I think that's a critical point: lining up the try that way, or factoring it out as in the block, implies a false parallelism. It implies that what's important about these statements is that they all try. That's typically not the most important thing about the code and not what we should be focused on when reading it.
Suppose for sake of argument that AsCommit never fails and consequently does not return an error. Now we have:
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))
// vs
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)
// vs
try (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
)
What you see at first glance is that the middle two lines are clearly different from the others. Why? It turns out because of error handling. Is that the most important detail about this code, the thing you should notice at first glance? My answer is no. I think you should notice the core logic of what the program is doing first, and error handling later. In this example, the try statement and try block hinder that view of the core logic. For me, this suggests they are not the right syntax for these semantics.
That leaves the first four syntaxes, which are even more similar to each other:
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))
// vs
headRef := try r.Head()
parentObjOne := try headRef.Peel(git.ObjectCommit)
parentObjTwo := try remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try index.WriteTree()
tree := try r.LookupTree(treeOid)
// vs
headRef := r.Head()?
parentObjOne := headRef.Peel(git.ObjectCommit)?
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)?
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()?
tree := r.LookupTree(treeOid)?
// vs
headRef := r.Head?()
parentObjOne := headRef.Peel?(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel?(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree?()
tree := r.LookupTree?(treeOid)
It's hard to get too worked up about choosing one over the others. They all have their good and bad points. The most important advantages of the builtin form are that:
(1) the exact operand is very clear, especially compared to prefix-operator try x.y().z()
.
(2) tools that don't need to know about try can treat it as a plain function call, so for example goimports will work fine without any adjustments, and
(3) there is some room for future expansion and adjustment if needed.
It is entirely possible that after seeing real code using these constructs, we will develop a better sense for whether the advantages of one of the other three syntaxes outweigh these advantages of the function call syntax. Only experiments and experience can tell us this.
Thanks for all the clarification. The more i think the more i like the proposal and see how it fit the goals.
Why not use a function like recover()
instead of err
that we don't know from where it come ? It would be more consistent and maybe easier to implement.
func f() error {
defer func() {
if err:=error();err!=nil {
...
}
}()
}
edit: I never use named return, then it'll be strange for me to add named return just for this
@flibustenet, see also https://swtch.com/try.html#named for a few similar suggestions.
(Answering all of them: we could do that, but it's not strictly necessary given named results, so we might as well try to use the existing concept before deciding we need to provide a second way.)
An unintended consequence of try()
may be that projects abandon _go fmt_ in order to gain single-line error checks. That's almost all the benefits of try()
with none of the costs. I've done that for a few years; it works well.
But I'd rather be able to define a last-resort error handler for the package, and eliminate all the error checks which need it. What I'd define is not try()
.
@networkimprov, you seem to be coming from a different position than the Go users we are targeting, and your message would contribute more to the conversation if it carried additional detail or links so we can better understand your point of view.
It's unclear what "costs" you believe try has. And while you say that abandoning gofmt has "none of the costs" of try (whatever those are), you seem to be ignoring that gofmt's formatting is the one used by all programs that help rewrite Go source code, like goimports, eg, gorename, and so on. You abandon go fmt at the cost of abandoning those helpers, or at least putting up with substantial incidental edits to your code when you invoke them. Even so, if it works well for you to do so, that's great: by all means keep doing that.
It's also unclear what "define a last-resort error handler for the package" means or why it would be appropriate to apply an error-handling policy to an entire package instead of to a single function at a time. If the main thing you'd want to do in an error handler is add context, the same context would not be appropriate across the entire package.
@rsc, As you may have seen, while I suggested the try block syntax, I later reverted to the "no" side for this feature -- partly because I feel uncomfortable hiding one or more conditional error returns in a statement or function application. But let me clarify one point. In the try block proposal I explicitly allowed statements that don't need try
. So your last try block example would be:
try (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
)
This simply says that any errors returned within the try block are returned to the caller. If the control makes past the try block, there were no errors in the block.
You said
I think you should notice the core logic of what the program is doing first, and error handling later.
This exactly the reason I thought of a try block! What is factored out is not just the keyword but the error handling. I don't want to have to think about N different places that may generate errors (except when I am explicitly trying to handle specific errors).
Some more points that may be worth mentioning:
try(try(foo(try(bar)).fum())
are allowed. Such use may be frowned upon but their semantics need to be specified. In the try block case the compiler has to work harder to detect such uses and squeeze out all error handling to the try block level.return-on-error
instead of try
. This is easier to swallow at a block level!FWIW, I still don't think this is worth doing.
@rsc
[...]
The main one is the claim that because it is possible to write complex, unreadable code, such code will become ubiquitous. As @josharian noted, it is already “possible to write abominable code in Go.”
[...]headRef := try(r.Head()) parentObjOne := try(headRef.Peel(git.ObjectCommit)) parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit)) parentCommitOne := try(parentObjOne.AsCommit()) parentCommitTwo := try(parentObjTwo.AsCommit())
I understand your position on "bad code" is that we can write awful code today like the following block.
parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())
What are your thoughts on disallowing nested try
calls so that we can't accidentally write bad code?
If you disallow nested try
on the first version, you will be able to remove this limitation later if needed, it wouldn't be possible the other way around.
I discussed this point already but it seems relevant - code complexity should scale vertically, not horizontally.
try
as an expression encourages code complexity to scale horizontally by encouraging nested calls. try
as a statement encourages code complexity to scale vertically.
@rsc, to your questions,
My package-level handler of last resort -- when error is not expected:
func quit(err error) {
fmt.Fprintf(os.Stderr, "quit after %s\n", err)
debug.PrintStack() // because panic(err) produces a pile of noise
os.Exit(3)
}
Context: I make heavy use of os.File (where I've found two bugs: #26650 & #32088)
A package-level decorator adding basic context would need a caller
argument -- a generated struct which provides the results of runtime.Caller().
I wish the _go fmt_ rewriter would use existing formatting, or let you specify formatting per transformation. I make do with other tools.
The costs (i.e. drawbacks) of try()
are well documented above.
I'm honestly floored that the Go team offered us first check/handle
(charitably, a novel idea), and then the ternaryesque try()
. I don't see why you didn't issue an RFP re error handling, and then collect community comment on some of the resulting proposals (see #29860). There's a lot of wisdom out here you could leverage!
@rsc
Syntax
This discussion has identified six different syntaxes to write the same semantics from the proposal:
f := try(os.Open(file))
, from the proposal (builtin function)f := try os.Open(file)
, using a keyword (prefix keyword)f := os.Open(file)?
, like in Rust (call-suffix operator)f := os.Open?(file)
, suggested by @rogpeppe (call-infix operator)try f := os.Open(file)
, suggested by @thepudds (try statement)try ( f := os.Open(file); f.Close() )
, suggested by @bakul (try block)
try {error} {optional wrap func} {optional return args in brackets}
f, err := os.Open(file)
try err wrap { a, b }
... and, IMO, improving readability (through alliteration) as well as semantic accuracy:
f, err := os.Open(file)
relay err
or
f, err := os.Open(file)
relay err wrap
or
f, err := os.Open(file)
relay err wrap { a, b }
or
f, err := os.Open(file)
relay err { a, b }
I know advocating for relay versus try is easy to dismiss as off-topic, but I can just imagine attempting to explain how try is not trying anything and doesn't throw anything. It's not clear AND has baggage. relay
being a new term would allow a clear-minded explanation, and the description has a basis in circuitry (which is what this is all about anyway).
Edit to clarify:
Try can mean - 1. to experience something and then judge it subjectively 2. to verify something objectively 3. attempt to do something 4. fire off multiple control flows that can be interrupted and launch an interceptable notification if so
In this proposal, try is doing none of those. We are actually running a function. It is then rewiring the control flow based upon an error value. This is literally the definition of a protective relay. We are directly re-laying circuitry (i.e. short-circuiting the current function scope) according to the value of a tested error.
In the try block proposal I explicitly allowed statements that don't need try
The main advantage in Go's error handling that I see over the try-catch system of languages like Java and Python is that it's always clear which function calls may result in an error and which cannot. The beauty of try
as documented in the original proposal is that it can cut down on simple error handling boilerplate while still maintaining this important feature.
To borrow from @Goodwine 's examples, despite its ugliness, from an error handling perspective even this:
parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())
... is better than what you often see in try-catch languages
parentCommitOne := r.Head().Peel(git.ObjectCommit).AsCommit()
parentCommitTwo := remoteBranch.Reference.Peel(git.ObjectCommit).AsCommit()
... because you can still tell what parts of the code may divert control flow due to an error and which cannot.
I know that @bakul isn't advocating for this block syntax proposal anyway, but I think it brings up an interesting point about Go's error handling in comparison to others. I think it's important that any error handling proposal Go adopts should not obfuscate what parts of the code can and cannot error out.
I've written a little tool: tryhard
(which doesn't try very hard at the moment) operates on a file-by-file basis and uses simple AST pattern matching to recognize potential candidates for try
and to report (and rewrite) them. The tool is primitive (no type checking) and there's a decent chance for false positives, depending on prevalent coding style. Read the documentation for details.
Applying it to $GOROOT/src
at tip reports > 5000 (!) opportunities for try
. There may be plenty of false positives, but checking out a decent sample by hand suggests that most opportunities are real.
Using the rewrite feature shows how the code will look like using try
. Again, a cursory glance at the output shows significant improvement in my mind.
(Caution: The rewrite feature will destroy files! Use at your own risk.)
Hopefully this will provide some concrete insight into what code might look like using try
and lets us move past idle and unproductive speculation.
Thanks & enjoy.
I understand your position on "bad code" is that we can write awful code today like the following block.
parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()) parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())
My position is Go developers do a decent job writing clear code and that almost certainly the compiler is not the only thing standing in the way of you or your coworkers writing code that looks like that.
What are your thoughts on disallowing nested
try
calls so that we can't accidentally write bad code?
A large part of the simplicity of Go derives from the selection of orthogonal features that compose independently. Adding restrictions breaks orthogonality, composability, independence, and in doing so breaks the simplicity.
Today, it is a rule that if you have:
x := expression
y := f(x)
with no other use of x anywhere, then it is a valid program transformation to simplify that to
y := f(expression)
If we were to adopt a restriction on try expressions, then it would break any tool that assumed this was always a valid transformation. Or if you had a code generator that worked with expressions and might process try expressions, it would have to go out of its way to introduce temporaries to satisfy the restrictions. And so on and so on.
In short, restrictions add significant complexity. They need significant justification, not "let's see if anyone bumps into this wall and asks us to take it down".
I wrote a longer explanation two years ago at https://github.com/golang/go/issues/18130#issuecomment-264195616 (in the context of type aliases) that applies equally well here.
@bakul,
But let me clarify one point. In the try block proposal I explicitly allowed statements that _don't need_
try
.
Doing this would fall short of the second goal: "Both error checks and error handling must remain explicit, meaning visible in the program text. We do not want to repeat the pitfalls of exception handling."
The main pitfall of traditional exception handling is not knowing where the checks are. Consider:
try {
s = canThrowErrors()
t = cannotThrowErrors()
u = canThrowErrors() // a second call
} catch {
// how many ways can you get here?
}
If the functions were not so helpfully named, it can be very difficult to tell which functions might fail and which are guaranteed to succeed, which means you can't easily reason about which fragments of code can be interrupted by an exception and which cannot.
Compare this with Swift's approach, where they adopt some of the traditional exception-handling syntax but are actually doing error handling, with an explicit marker on each checked function and no way to unwind beyond the current stack frame:
do {
let s = try canThrowErrors()
let t = cannotThrowErrors()
let u = try canThrowErrors() // a second call
} catch {
handle error from try above
}
Whether it's Rust or Swift or this proposal, the key, critical improvement over exception handling is explicitly marking in the text - even with a very lightweight marker - each place where a check is.
For more about the problem of implicit checks, see the Problem section of the problem overview from last August, in particular the links to the two Raymond Chen articles.
Edit: see also @velovix's comment three up, which came in while I was working on this one.
@daved, I'm glad the "protective relay" analogy works for you. It doesn't work for me. Programs are not circuits.
Any word can be misunderstood:
"break" does not break your program.
"continue" doesn't continue execution at the next statement like normal.
"goto" ... well goto is impossible to misunderstand actually. :-)
https://www.google.com/search?q=define+try says "make an attempt or effort to do something" and "subject to trial". Both of those apply to "f := try(os.Open(file))". It attempts to do the os.Open (or, it subjects the error result to trial), and if the attempt (or the error result) fails, it returns from the function.
We used check last August. That was a good word too. We switched to try, despite the historical baggage of C++/Java/Python, because the current meaning of try in this proposal matches the meaning in Swift's try (without the surrounding do-catch) and in Rust's original try!. It won't be terrible if we decide later that check is the right word after all but for now we should focus on things other than the name.
Here's an interesting tryhard
false negative, from github.com/josharian/pct
. I mention it here because:
try
detection is trickyif err != nil
impacts how people (me at least) structure their code, and that try
can help with thatBefore:
var err error
switch {
case *flagCumulative:
_, err = fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s)
case *flagQuiet:
_, err = fmt.Fprintln(w, line.s)
default:
_, err = fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s)
}
if err != nil {
return err
}
After (manual rewrite):
switch {
case *flagCumulative:
try(fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s))
case *flagQuiet:
try(fmt.Fprintln(w, line.s))
default:
try(fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s))
}
Change https://golang.org/cl/182717 mentions this issue: src: apply tryhard -r $GOROOT/src
For a visual idea of try
in the std library, head over to CL 182717.
Thanks, @josharian, for this. Yes, it may impossible even for a good tool to detect all possible use candidates for try
. But luckily that is not the primary goal (of this proposal). Having a tool is useful, but I see the main benefit of try
in code that's not yet written (because there's going to be much more of that than code that we already have).
"break" does not break your program.
"continue" doesn't continue execution at the next statement like normal.
"goto" ... well goto is impossible to misunderstand actually. :-)
break
does break the loop. continue
does continue the loop, and goto
does go to the indicated destination. Ultimately, I do hear you, but please consider what happens when a function mostly completes and returns an error, but does not rollback. It was not a try/trial. I do think check
is far superior in that regard (to "halt the progress of" through "examination" is certainly apt).
More pertinent, I am curious about the form of try/check that I offered as opposed to the other syntaxes.
try {error} {optional wrap func} {optional return args in brackets}
f, err := os.Open(file)
try err wrap { a, b }
The standard library ends up not being representative of "real" Go code in that it doesn't spend much time coordinating or connecting other packages. We've noticed this in the past as the reason why there is so little channel usage in the standard library compared to packages farther up the dependency food chain. I suspect error handling and propagation ends up being similar to channels in this respect: you'll find more the higher up you go.
For this reason, it would be interesting for someone to run tryhard on some larger application code bases and see what fun things can be discovered in that context. (The standard library is interesting too, but as more of a microcosm than an accurate sampling of the world.)
I am curious about the form of try/check that I offered as opposed to the other syntaxes.
I think that form ends up recreating existing control structures.
@networkimprov, re https://github.com/golang/go/issues/32437#issuecomment-502879351
I'm honestly floored that the Go team offered us first check/handle (charitably, a novel idea), and then the ternaryesque try(). I don't see why you didn't issue an RFP re error handling, and then collect community comment on some of the resulting proposals (see #29860). There's a lot of wisdom out here you could leverage!
As we discussed in #29860, I honestly don't see much difference between what you are suggesting we should have done as far as soliciting community feedback and what we actually did. The draft designs page explicitly says they are "starting points for discussion, with an eventual goal of producing designs good enough to be turned into actual proposals." And people did write many things ranging from short feedback to full alternate proposals. And most of it was helpful and I appreciate your help in particular in organizing and summarizing. You seem to be fixated on calling it a different name or introducing additional layers of bureaucracy, which as we discussed on that issue we don't really see a need for.
But please don't claim that we somehow did not solicit community advice or ignored it. That's simply not true.
I also can't see how try is in any way "ternaryesque", whatever that would mean.
Agreed, I think that was my goal; I don't think more complex mechanisms are worthwhile. If I were in your shoes, the most I'd offer is a bit of syntactic sugar to silence the majority of complaints and no more.
@rsc, apologies for veering off-topic!
I raised package-level handlers in https://github.com/golang/go/issues/32437#issuecomment-502840914
and responded to your request for clarification in https://github.com/golang/go/issues/32437#issuecomment-502879351
I see package-level handlers as a feature that virtually everyone could get behind.
please use try {} catch{} syntax, don't build more wheels
please use try {} catch{} syntax, don't build more wheels
i think it's appropriate to build better wheels when the wheels that other people use are shaped like squares
@jimwei
Exception-based error handling might be a pre-existing wheel but it also has quite a few known problems. The problem statement in the original draft design does a great job of outlining these issues.
To add my own less well thought out commentary, I think it's interesting that many very successful newer languages (namely Swift, Rust, and Go) have not adopted exceptions. This tells me that the broader software community is rethinking exceptions after the many years we've had to work with them.
In response to https://github.com/golang/go/issues/32437#issuecomment-502837008 (@rsc's comment about try
as a statement)
You raise a good point. I'm sorry that I had somehow missed that comment before making this one: https://github.com/golang/go/issues/32437#issuecomment-502871889
Your examples with try
as an expression look much better than the ones with try
as a statement. The fact that the statement leads with try
does in fact make it much harder to read. However, I am still worried that people will nest try calls together to make bad code, as try
as an expression really _encourages_ this behavior in my eyes.
I think I would appreciate this proposal a bit more if golint
prohibited nested try
calls. I think that prohibiting all try
calls inside of other expressions is a bit too strict, having try
as an expression does have its merits.
Borrowing your example, even just nesting 2 try calls together looks quite hideous, and I can see Go programmers doing it, especially if they work without code reviewers.
parentCommitOne := try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit()
parentCommitTwo := try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()
tree := try(r.LookupTree(try(index.WriteTree())))
The original example actually looked quite nice, but this one shows that nesting the try expressions (even only 2-deep) really does hurt the readability of the code drastically. Denying nested try
calls would also help with the "debuggability" issue, since it's much easier to expand a try
into an if
if it's on the outside of an expression.
Again, I'd almost like to say that a try
inside a sub-expression should be flagged by golint
, but I think that might be a little too strict. It would also flag code like this, which in my eyes is fine:
x := 5 + try(strconv.Atoi(input))
This way, we get both the benefits of having try
as an expression, but we aren't promoting adding too much complexity to the horizontal axis.
Perhaps another solution would be that golint
should only allow a maximum of 1 try
per statement, but it's late, I'm getting tired, and I need to think about it more rationally. Either way, I have been quite negative toward this proposal at some points, but I think I can actually turn to really liking it as long as there are some golint
standards related to it.
@rsc
We can and should distinguish between _"this feature can be used for writing very readable code, but may also be abused to write unreadable code"_ and "the dominant use of this feature will be to write unreadable code".
Experience with C suggests that ? : falls squarely into the second category. (With the possible exception of min and max,
What first struck me about try()
— vs try
as a statement — was how similar it was in nestability to the ternary operator and yet how opposite the arguments for try()
and against ternary were _(paraphrased):_
try():
_"You can nest it, but we doubt many will because most people want to write good code"_, Respectfully, that rational for the difference between the two feels so subjective I would ask for some introspection and at least consider if you might be rationalizing a difference for feature you prefer vs. against a feature you dislike? #please_dont_shoot_the_messenger
_"I'm not sure I've ever seen code using ? : that was not improved by rewriting it to use an if statement instead. But this paragraph is getting off topic.)"_
In other languages I frequently improve statements by rewriting them from an if
to a ternary operator, e.g. from code I wrote today in PHP:
return isset( $_COOKIE[ CookieNames::CART_ID ] )
? intval( $_COOKIE[ CookieNames::CART_ID ] )
: null;
Compare to:
if ( isset( $_COOKIE[ CookieNames::CART_ID ] ) ) {
return intval( $_COOKIE[ CookieNames::CART_ID ] );
} else {
return null;
}
As far as I am concerned, the former is much improved over the latter.
I think the criticism against this proposal is largely due to high expectations that were raised by the previous proposal, which would have been a lot more comprehensive. However, I think such high expectations were justified for reasons of consistency. I think what many people would have liked to see, is a single, comprehensive construct for error handling that is useful in all use cases.
Compare this feature, for instance, with the built in append()
function. Append was created because appending to slice was a very common use case, and while it was possible to do it manually it was also easy to do it wrong. Now append()
allows to append not just one, but many elements, or even a whole slice, and it even allows to append a string to a []byte slice. It is powerful enough to cover all use cases of appending to a slice. And hence, no one appends slices manually anymore.
However, try()
is different. It is not powerful enough so we can use it in all cases of error handling. And I think that's the most serious flaw of this proposal. The try()
builtin function is only really useful, in the sense that it reduces boilerplate, in the most simple of cases, namely just passing on an error to the caller, and with a defer statement, if all errors of the function need to be handled in the same way.
For more complex error handling, we will still need to use if err != nil {}
. This then leads to two distinct styles for error handling, where before there was only one. If this proposal is all we get to help with error handling in Go, then, I think it would be better to do nothing and keep handling error handling with if
like we always have, because at least, this is consistent and had the benefit of "there is only one way to do it".
@rsc, apologies for veering off-topic!
I raised package-level handlers in #32437 (comment)
and responded to your request for clarification in #32437 (comment)I see package-level handlers as a feature that virtually everyone could get behind.
I don't see what ties together the concept of a package with specific error handling. It's hard to imagine the concept of a package-level handler being useful to, say, net/http
. In a similar vein, despite writing smaller packages than net/http
in general, I cannot think of a single use case where I would have preferred a package-level construct to do error handling. In general, I've found that the assumption that everyone shares one's experiences, use cases, and opinions is a dangerous one :)
@beoran i believe this proposal make further improvement possibles. Like a decorator at last argument of try(..., func(err) error)
, or a tryf(..., "context of my error: %w")
?
@flibustenet While such later extensions could be possible, the proposal as it is now seems to discourage such extensions, mostly because adding an error handler would be redundant with defer.
I guess the difficult problem is how to have comprehensive error handling without duplicating the functionality of defe. Perhaps the defer statement itself could enhanced somehow to allow easier error handling in more complex cases... But, that is a different issue.
https://github.com/golang/go/issues/32437#issuecomment-502975437
This then leads to two distinct styles for error handling, where before there was only one. If this proposal is all we get to help with error handling in Go, then, I think it would be better to do nothing and keep handling error handling with
if
like we always have, because at least, this is consistent and had the benefit of "there is only one way to do it".
@beoran Agreed. This is why I suggested that we unify the vast majority of error cases under the try
keyword (try
and try
/else
). Even though the try
/else
syntax doesn't give us any significant reduction in code length versus the existing if err != nil
style, it gives us consistency with the try
(no else
) case. Those two cases (try and try-else) are likely to cover the vast majority of error handling cases. I put that in opposition to the builtin no-else version of try
that only applies in cases where the programmer isn't actually doing anything to handle the error besides returning (which, as others have mentioned in this thread, isn't necessarily something we really want to encourage in the first place).
Consistency is important to readability.
append
is the definitive way to add elements to a slice. make
is the definitive way to construct a new channel or map or slice (with the exception of literals, which I'm not thrilled about). But try()
(as a builtin, and without else
) would be sprinkled throughout codebases, depending on how the programmer needs to handle a given error, in a way that's probably a bit chaotic and confusing to the reader. It doesn't seem to be in the spirit of the other builtins (namely, handling a case that's either quite difficult or outright impossible to do otherwise). If this is the version of try
that succeeds, consistency and readability will compel me not to use it, just as I try to avoid map/slice literals (and avoid new
like the plague).
If the idea is to change how errors are handled, it seems wise to try to unify the approach across as many cases as possible, rather than adding something that, at best, will be "take it or leave it." I fear the latter will actually add noise rather than reducing it.
@deanveloper wrote:
I think I would appreciate this proposal a bit more if golint prohibited nested try calls.
I agree that deeply nested try
could be hard to read. But this is also true for standard function calls, not just the try
built-in function. Thus I don't see why golint
should forbid this.
@brynbellomy wrote:
Even though the try/else syntax doesn't give us any significant reduction in code length versus the existing if err != nil style, it gives us consistency with the try (no else) case.
The unique goal of the try
built-in function is to reduce boilerplate, so it's hard to see why we should adopt the try/else syntax you propose when you acknowledge that it "doesn't give us any significant reduction in code length".
You also mention that the syntax you propose makes the try case consistent with the try/else case. But it also creates an inconsistent way to branch, when we already have if/else. You gain a bit of consistency on a specific use case but lose a lot inconsistency on the rest.
I feel the need to express my opinions for what they are worth. Though not all of this is academic and technical in nature, I think it needs to be said.
I believe this change is one of these cases where engineering is being done for engineering sake and "progress" is being used for the justification. Error handling in Go is not broken and this proposal violates a lot of the design philosophy I love about Go.
Make things easy to understand, not easy to do
This proposal is choosing optimizing for laziness over correctness. The focus is on making error handling easier and in return a huge amount of readability is being lost. The occasional tedious nature of error handling is acceptable because of the readability and debuggability gains.
Avoid naming return arguments
There are a few edge cases with defer
statements where naming the return argument is valid. Outside of these, it should be avoided. This proposal promotes the use of naming return arguments. This is not going to help make Go code more readable.
Encapsulation should create a new semantics where one is absolutely precise
There is no precision in this new syntax. Hiding the error variable and the return does not help to make things easier to understand. In fact, the syntax feels very foreign from anything we do in Go today. If someone wrote a similar function, I believe the community would agree the abstraction is hiding the cost and not worth the simplicity it's trying to provide.
Who are we trying to help?
I am concerned this change is being put in place in an attempt to entice enterprise developers away from their current languages and into Go. Implementing language changes, just to grow numbers, sets a bad precedent. I think it's fair to ask this question and get an answer to the business problem that is attempting to be solved and the expected gain that is trying to be achieved?
I have seen this before several times now. It seems quite clear, with all the recent activity from the language team, this proposal is basically set in stone. There is more defending of the implementation then actual debate on the implementation itself. All of this started 13 days ago. We will see the impact this change has on the language, community and future of Go.
Error handling in Go is not broken and this proposal violates a lot of the design philosophy I love about Go.
Bill expresses my thoughts perfectly.
I can't stop try
being introduced, but if it is, I won't be using it myself; I won't teach it, and I won't accept it in PRs I review. It will simply be added to the list of other 'things in Go I never use' (see Mat Ryer's amusing talk on YouTube for more of these).
@ardan-bkennedy, thanks for your comments.
You asked about the "business problem that is attempting to be solved". I don't believe we are targeting the problems of any particular business except maybe "Go programming". But more generally we articulated the problem we are trying to solve last August in the Gophercon design draft discussion kickoff (see the Problem Overview especially the Goals section). The fact that this conversation has been going on since last August also flatly contradicts your claim that "All of this started 13 days ago."
You are not the only person to have suggested that this is not a problem or not a problem worth solving. See https://swtch.com/try.html#nonissue for other such comments. We have noted those and do want to make sure we are solving an actual problem. Part of the way to find out is to evaluate the proposal on real code bases. Tools like Robert's tryhard help us do that. I asked earlier for people to let us know what they find in their own code bases. That information will be critically important to evaluating whether the change is worthwhile or not. You have one guess and I have a different one, and that's fine. The answer is to substitute data for those guesses.
We will do what is needed to make sure we are solving an actual problem. We're not going to go through the effort of adding a language feature that will make Go programming worse overall.
Again, the path forward is experimental data, not gut reactions. Unfortunately, data takes more effort to collect. At this point, I would encourage people who want to help to go out and collect data.
@ardan-bkennedy, sorry for the second followup but regarding:
I am concerned this change is being put in place in an attempt to entice enterprise developers away from their current languages and into Go. Implementing language changes, just to grow numbers, sets a bad precedent.
There are two serious problems with this line that I can't walk past.
First, I reject the implicit claim that there are classes of developers – in this case "enterprise developers" – that are somehow not worthy of using Go or having their problems considered. In the specific case of "enterprise", we are seeing plenty of examples of both small and large companies using Go very effectively.
Second, from the start of the Go project, we – Robert, Rob, Ken, Ian, and I – have evaluated language changes and features based on our collective experience building many systems. We ask "would this work well in the programs we write?" That has been a successful recipe with broad applicability and is the one we intend to keep using, again augmented by the data I asked for in the previous comment and experience reports more generally. We would not suggest or support a language change that we can't see ourselves using in our own programs or that we don't think fits well into Go. And we would certainly not suggest or support a bad change just to have more Go programmers. We use Go too after all.
@rsc
There will be no shortage of locations where this convenience can be placed. What metric is being sought that will prove the substance of the mechanism aside from that? Is there a list of classified error handling cases? How will value be derived from the data when much of the public process is driven by sentiment?
The tools tryhard
is very informative !
I could see that i use often return ...,err
, but only when i know that i call a function that already wrap the error (with pkg/errors
), mostly in http handlers. I win in readability with fewer line of code.
Then in theses http handler i would add a defer fmt.HandleErrorf(&err, "handler xyz")
and finally add more context than before.
I see also lot of case where i don't care of the error at all fmt.Printf
and i will do it with try
.
Will it be possible for example to do defer try(f.Close())
?
So, maybe try
will finally help to add context and push best practice rather than the opposite.
I'm very impatient to test in real !
@flibustenet The proposal as is won't allow defer try(f())
(see the rationale). There's all kinds of problems with that.
When using this tryhard
tool to see changes in a codebase, could we also compare the ratio of if err != nil
before and after to see whether it's more common to add context or to just pass the error back?
My thinking is that maybe a hypothetical huge project can see 1000 places where try()
was added but there are 10000 if err != nil
that add context so even though 1000 looks huge, it's only 10% of the full thing.
@Goodwine Yes. I probably won't get to make this change this week, but the code is pretty straight-forward and self-contained. Feel free to give it a try (no pun intended), clone, and adjust as needed.
Wouldn't defer try(f())
be equivalent to
defer func() error {
if err:= f(); err != nil { return err }
return nil
}()
This (the if version) is currently not disallowed, right? Seems to me you should not make an exception here -- may be generate a warning? And it is not clear if the defer code above is necessarily wrong. What if close(file)
fails in a defer
statement? Should we report that error or not?
I read the rationale which seems to talk about defer try(f)
not defer try(f())
. May be a typo?
A similar argument can be made for go try(f())
, which translates to
go func() error {
if err:= f(); err != nil { return err }
return nil
}()
Here try
doesn't do anything useful but is harmless.
@ardan-bkennedy Thanks for your thoughts. With all due respect, I believe you have misrepresented the intent of this proposal and made several unsubstantiated claims.
Regarding some of the points @rsc has not addressed earlier:
We have never said error handling is broken. The design is based on the observation (by the Go community!) that current handling is fine, but verbose in many cases - this is undisputed. This is a major premise of the proposal.
Making things easier to do can also make them easier to understand - these two don't mutually exclude each other, or even imply one another. I urge you to look at this code for an example. Using try
removes a significant amount of boilerplate, and that boilerplate adds virtually nothing to the understandability of the code. Factoring out of repetitive code is a standard and widely accepted coding practice for improving code quality.
Regarding "this proposal violates a lot of the design philosophy": What is important is that we don't get dogmatic about "design philosophy" - that is often the downfall of good ideas (besides, I think we know a thing or two about Go's design philosophy). There is a lot of "religious fervor" (for lack of a better term) around named vs unnamed result parameters. Mantras such as "you shall not use named result parameters ever" out of context are meaningless. They may serve as general guidelines, but not absolute truths. Named result parameters are not inherently "bad". Well-named result parameters can add to the documentation of an API in meaningful ways. In short, let's not use slogans to make language design decisions.
It is a point of this proposal to not introduce new syntax. It just proposes a new function. We can't write that function in the language, so a built-in is the natural place for it in Go. Not only is it a simple function, it is also defined very precisely. We choose this minimal approach over more comprehensive solutions exactly because it does one thing very well and leaves almost nothing to arbitrary design decisions. We are also not wildly off the beaten track since other languages (e.g. Rust) have very similar constructs. Suggesting that the "the community would agree the abstraction is hiding the cost and not worth the simplicity it's trying to provide" is putting words into other people's mouth. While we can clearly hear the vocal opponents of this proposal, there is significant percentage (an estimated 40%) of people who expressed approval of going forward with the experiment. Let's not disenfranchise them with hyperbole.
Thanks.
return isset( $_COOKIE[ CookieNames::CART_ID ] )
? intval( $_COOKIE[ CookieNames::CART_ID ] )
: null;
Pretty sure this should be return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null;
FWIW. 😁
@bakul because arguments are evaluated immediately, it is actually roughly equivalent to:
<result list> := f()
defer try(<result list>)
This may be unexpected behavior to some as the f()
is not defered for later, it is executed right away. Same thing applies to go try(f())
.
@bakul The doc mentions defer try(f)
(rather than defer try(f())
because try
in general applies to any expression, not just a function call (you can say try(err)
for instance, if err
is of type error
). So not a typo, but perhaps confusing at first. f
simply stands for an expression, which usually happens to be a function call.
@deanveloper, @griesemer Never mind :-) Thanks.
@carl-mastrangelo
_"Pretty sure this should be
return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null;
_
You are assuming PHP 7.x. I was not. But then again, given your snarky face, you know that was not the point. :wink:
I am preparing a short demonstration to display this discussion during a go meetup taking place tomorrow, and hear some new thoughts, as I believe most participants on this thread (contributors or watchers), are those who are involved more deeply in the language, and most likely "not the average go developer" (just a hunch).
While doing that, I remembered we actually had a meetup about errors and a discussion on two patterns:
type ExtErr struct{
error
someOtherField string
}
These are used in a few stacks my teams actually built.
The proposal Q&A states
Q: The last argument passed to try must be of type error. Why is it not sufficient for the incoming argument to be assignable to error?
A: "... We can revisit this decision in the future if necessary"
Can anyone comment of similar use cases so we can understand if this need is common for both above error extending options ?
@mikeschinkel I'm not the Carl you're looking for.
@daved, re:
There will be no shortage of locations where this convenience can be placed. What metric is being sought that will prove the substance of the mechanism aside from that? Is there a list of classified error handling cases? How will value be derived from the data when much of the public process is driven by sentiment?
The decision is based on how well this works in real programs. If people show us that try is ineffective in the bulk of their code, that's important data. The process is driven by that kind of data. It is _not_ driven by sentiment.
Error Context
The most important semantic concern that's been raised in this issue is whether try will encourage better or worse annotation of errors with context.
The Problem Overview from last August gives a sequence of example CopyFile implementations in the Problem and Goals sections. It is an explicit goal, both back then and today, that any solution make it _more likely_ that users add appropriate context to errors. And we think that try can do that, or we wouldn't have proposed it.
But before we get to try, it is worth making sure we're all on the same page about appropriate error context. The canonical example is os.Open. Quoting the Go blog post “Error handling and Go”:
It is the error implementation's responsibility to summarize the context.
The error returned by os.Open formats as "open /etc/passwd: permission denied," not just "permission denied."
See also Effective Go's section on Errors.
Note that this convention may differ from other languages you are familiar with, and it is also only inconsistently followed in Go code. An explicit goal of trying to streamline error handling is to make it easier for people to follow this convention and add appropriate context, and thereby to make it followed more consistently.
There is lots of code following the Go convention today, but there is also lots of code assuming the opposite convention. It's too common to see code like:
f, err := os.Open(file)
if err != nil {
log.Fatalf("opening %s: %v", file, err)
}
which of course prints the same thing twice (many examples in this very discussion look like this). Part of this effort will have to be making sure everyone knows about and is following the convention.
In code following the Go error context convention, we expect that most functions will properly add the same context to each error return, so that one decoration applies in general. For example, in the CopyFile example, what needs to be added in each case is details about what was being copied. Other specific returns might add more context, but typically in addition rather than in replacement. If we're wrong about this expectation, that would be good to know. Clear evidence from real code bases would help.
The Gophercon check/handle draft design would have used code like:
func CopyFile(src, dst string) error {
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
...
}
This proposal has revised that, but the idea is the same:
func CopyFile(src, dst string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}()
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
...
}
and we want to add an as-yet-unnamed helper for this common pattern:
func CopyFile(src, dst string) (err error) {
defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
...
}
In short, the reasonability and success of this approach depends on these assumptions and logical steps:
If there is an assumption or logical step that you think is false, we want to know. And the best way to tell us is to point to evidence in actual code bases. Show us common patterns you have where try is inappropriate or makes things worse. Show us examples of things where try was more effective than you expected. Try to quantify how much of your code base falls on one side or the other. And so on. Data matters.
Thanks.
Thanks @rsc for the additional info on error context best practice. This point on best practice in particular has alluded me, but significantly improves try
s relationship to error context.
Therefore most functions only need to add function-level context describing the overall
operation, not the specific sub-piece that failed (that sub-piece self-reported already).
So then the place where try
does not help is when we need to react to errors, not just contextualize them.
To adapt an example from Cleaner, more elegant, and wrong, here their example of a function that is subtly wrong in its error handling. I've adapted it to Go using try
and defer
-style error wrapping:
func AddNewGuy(name string) (guy Guy, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("adding guy %v: %v", name, err)
}
}()
guy = Guy{name: name}
guy.Team = ChooseRandomTeam()
try(guy.Team.Add(guy))
try(AddToLeague(guy))
return guy, nil
}
This function is incorrect because if guy.Team.Add(guy)
succeeds but AddToLeague(guy)
fails, the team will have an invalid Guy object that isn't in a league. The correct code would look like this, where we roll back guy.Team.Add(guy)
and can no longer use try
:
func AddNewGuy(name string) (guy Guy, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("adding guy %v: %v", name, err)
}
}()
guy = Guy{name: name}
guy.Team = ChooseRandomTeam()
try(guy.Team.Add(guy))
if err := AddToLeague(guy); err != nil {
guy.Team.Remove(guy)
return Guy{}, err
}
return guy, nil
}
Or, if we want to avoid having to provide zero values for the non-error return values, we can replace return Guy{}, err
with try(err)
. Regardless, the defer
-ed function is still run and context is added, which is nice.
Again, this means that try
punts on reacting to errors, but not on adding context to them. That's a distinction that has alluded me and perhaps others. This makes sense because the way a function adds context to an error is not of particular interest to a reader, but the way a function reacts to errors is important. We should be making the less interesting parts of our code less verbose, and that's what try
does.
You are not the only person to have suggested that this is not a problem or not a problem worth solving. See https://swtch.com/try.html#nonissue for other such comments. We have noted those and do want to make sure we are solving an actual problem.
@rsc I also think there is no problem with current error code. So, please, count me in.
Tools like Robert's tryhard help us do that. I asked earlier for people to let us know what they find in their own code bases. That information will be critically important to evaluating whether the change is worthwhile or not. You have one guess and I have a different one, and that's fine. The answer is to substitute data for those guesses.
I looked at https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go and I like old code better. It is surprising to me that try function call might interrupt current execution. That is not how current Go works.
I suspect, you will find opinions will vary. I think this is very subjective.
And, I suspect, majority of users are not participating in this debate. They don't even know that this change is coming. I am pretty involved with Go myself, but I don't participate in this change, because I have no free time.
I think we would need to re-educate all existing Go users to think differently now.
We would also need to decide what to do with some users / companies who will refuse to use try in their code. There will be some for sure.
Maybe we would have to change gofmt to rewrite current code automatically. To force such "rogue" users to use new try function. Is it possible to make gofmt do that?
How would we deal with compile errors when people use go1.13 and before to build code with try?
I probably missed many other problems we would have to overcome to implement this change. Is it worth the trouble? I don't believe so.
Alex
@griesemer
While trying tryhard on a file with 97 err's none caught, I found that the 2 patterns not translated
1 :
if err := updateItem(tx, fields, entityView.DataBinding, entityInstance); err != nil {
tx.Rollback()
return nil, err
}
Is not replaced, probably because the tx.Rollback() between err := and the return line,
Which I assume can only should be handled by defer - and if all paths of error needs to tx.Rollback()
Is this right ?
if err := db.Error; err != nil {
return nil, err
} else if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
return nil, err
} else {
return itemDb, nil
}
or
if err := db.Error; err != nil {
return nil, err
} else {
if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
return nil, err
} else {
return itemDb, nil
}
return result, nil
}
Is this because of the shadowing or the nesting try would translate to ? meaning - should this use try or suggested to be left as err := ... return err ?
@guybrand Re: the two patterns you found:
1) yes, tryhard
doesn't try very hard. type-checking is necessary for more complex cases. If tx.Rollback()
should be done in all paths, defer
might be the right approach. Otherwise, keeping the if
might be the right approach. It depends on the specific code.
2) Same here: tryhard
doesn't look for this more complex pattern. Maybe it could.
Again, this is an experimental tool to get some quick answers. Doing it right requires a bit more work.
@alexbrainman
How would we deal with compile errors when people use go1.13 and before to build code with try?
My understanding is the the version of the language itself will be controlled by the go
language version directive in the go.mod
file for each piece of code being compiled.
The in-flight go.mod
documentation describes the go
language version directive like so:
The expected language version, set by the
go
directive, determines
which language features are available when compiling the module.
Language features available in that version will be available for use.
Language features removed in earlier versions, or added in later versions,
will not be available. Note that the language version does not affect
build tags, which are determined by the Go release being used.
If hypothetically something like a new try
builtin lands in something like Go 1.15, then at that point someone whose go.mod
file reads go 1.12
would not have access to that new try
builtin even if they compile with the Go 1.15 toolchain. My understanding of the current plan is that they would need to change the Go language version declared in their go.mod
from go 1.12
to instead read go 1.15
if they want to use the new Go 1.15 language feature of try
.
On the other hand, if you have code that uses try
and that code lives in a module whose go.mod
file declares its Go language version as go 1.15
, but then someone attempts to build that with the Go 1.12 toolchain, at that point the Go 1.12 toolchain will fail with a compile error. The Go 1.12 toolchain does not know anything about try
, but it knows enough to print an additional message that the code that failed to compile claimed to require Go 1.15 based on what is in the go.mod
file. You can actually attempt this experiment right now using today's Go 1.12 toolchain, and see the resulting error message:
.\hello.go:3:16: undefined: try
note: module requires Go 1.15
There is a much longer discussion in the Go2 transitions proposal document.
That said, the exact details of that might be better discussed elsewhere (e.g., perhaps in #30791, or this recent golang-nuts thread).
@griesemer , sorry if I missed a more specific request for a format, but I would love to share some results, and have access (a possible permission) to some companies' source code.
Below is a real example for a small project, I think the attached results gives a good sample, if so, we can probably share some table with similar results:
Total = Number of code lines
$find /path/to/repo -name '*.go' -exec cat {} \; | wc -l
Errs = number of lines with err := (this probably misses err = , and myerr := , but I think in most cases it covers)
$find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
tryhard = number of lines tryhard found
the first case I tested to study returned:
Total = 5106
Errs = 111
tryhard = 16
bigger code base
Total = 131777
Errs = 3289
tryhard = 265
If this format is acceptable, let us know how you want to get the results, I assume just throwing it here would not be the correct format
Also, it would probably be a quickie to have tryhard count the lines, occasions of err := (and probably err = , only 4 on the code base I tried to learn upon)
Thanks.
From @griesemer in https://github.com/golang/go/issues/32437#issuecomment-503276339
I urge you to look at this code for an example.
In regards to that code, I noticed that the out file created here never seems to be closed. Additionally, it's important to check errors from closing files you've written to, because that may be the only time you're informed that there was a problem with a write.
I'm bringing this up not as a bug report (though maybe it should be?), but as a chance to see if try
has an effect on how one might fix it. I'll enumerate all of the ways that I can think of to fix it and consider if the addition of try
would help or hurt. Here are some ways:
outf.Close()
right before any error return.func foo() (err error) {
outf := try(os.Create())
defer func() {
cerr := outf.Close()
if err == nil {
err = cerr
}
}()
...
}
defer outf.Close()
to ensure resource cleanup, and try(outf.Close())
before returning to ensure no errors.func foo() error {
outf := try(os.Create())
if err := helper(outf); err != nil {
outf.Close()
return err
}
try(outf.Close())
return nil
}
I think in all cases except case number 1, try
is at worst neutral and usually positive. And I'd consider number 1 to be the least palatable option given the size and number of error possibilities in that function, so adding try
would reduce the appeal of a negative choice.
I hope this analysis was useful.
If hypothetically something like a new
try
builtin lands in something like Go 1.15, then at that point someone whosego.mod
file readsgo 1.12
would not have access
@thepudds thank you for explaining. But I don't use modules. So your explanation is way over my head.
Alex
@alexbrainman
How would we deal with compile errors when people use go1.13 and before to build code with try?
If try
was to hypothetically land in something like Go 1.15, then the very short answer to your question is that someone using Go 1.13 to build code with try
would see a compile error like this:
.\hello.go:3:16: undefined: try
note: module requires Go 1.15
(At least as far as I understand what’s been stated about the transition proposal).
@alexbrainman Thanks for your feedback.
A large number of comments on this thread are of the form "this doesn't look like Go", or "Go doesn't work like that", or "I'm not expecting this to happen here". That is all correct, _existing_ Go doesn't work like that.
This is perhaps the first suggested language change that affects the feel of the language in more substantial ways. We are aware of that, which is why we kept it so minimal. (I have a hard time imagining the uproar a concrete generics proposal might cause - talking about a language change).
But going back to your point: Programmers get used to how a programming language works and feels. If I've learned anything over the course of some 35 years of programming is that one gets used to almost any language, and it happens very quickly. After having learned original Pascal as my first high-level language, it was _inconceivable_ that a programming language would not capitalize all its keywords. But it only took a week or so to get used to the "sea of words" that was C where "one couldn't see the structure of the code because it's all lowercase". After those initial days with C, Pascal code looked awfully loud, and all the actual code seemed buried in a mess of shouting keywords. Fast forward to Go, when we introduced capitalization to mark exported identifiers, it was a shocking change from the prior, if I remember correctly, keyword-based approach (this was before Go was public). Now we think it's one of the better design decisions (with the concrete idea actually coming from outside the Go Team). Or, consider the following thought experiment: Imagine Go had no defer
statement and now somebody makes a strong case for defer
. defer
doesn't have semantics like anything else in the language, the new language doesn't feel like that pre-defer
Go anymore. Yet, after having lived with it for a decade it seems totally "Go-like".
The point is, the initial reaction towards a language change is almost meaningless without actually trying the mechanism in real code and gathering concrete feedback. Of course, the existing error handling code is fine and looks clearer than the replacement using try
- we've been trained to think those if
statements away for a decade now. And of course try
code looks strange and has "weird" semantics, we have never used it before, and we don't immediately recognize it as a part of the language.
Which is why we are asking people to actually engage with the change by experimenting with it in your own code; i.e., actually writing it, or have tryhard
run over existing code, and consider the result. I'd recommend to let it sit for a while, perhaps a week or so. Look at it again, and report back.
Finally, I agree with your assessment that a majority of people don't know about this proposal, or have not engaged with it. It is quite clear that this discussion is dominated by perhaps a dozen or so people. But it's still early, this proposal has only been out for two weeks, and no decision has been made. There is plenty of time for more and different people to engage with this.
https://github.com/golang/go/issues/32437#issuecomment-503297387 pretty much says if you're wrapping errors in more than one way in a single function, you're apparently doing it wrong. Meanwhile, I have a lot of code that looks like this:
if err := gen.Execute(tmp, s); err != nil {
return fmt.Errorf("template error: %v", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("cannot write temp file: %v", err)
}
closed = true
if err := os.Rename(tmp.Name(), *genOutput); err != nil {
return fmt.Errorf("cannot finalize file: %v", err)
}
removed = true
(closed
and removed
are used by defers to clean up, as appropriate)
I really don't think all of these should just be given the same context describing the top-level mission of this function. I really don't think the user should just see
processing path/to/dir: template: gen:42:17: executing "gen" at <.Broken>: can't evaluate field Broken in type main.state
when the template is screwed up, I think it's the responsibility of my error handler for the template Execute call to add "executing template" or some such little extra bit. (That's not the greatest bit of context, but I wanted to copy-paste real code instead of a made-up example.)
I don't think the user should see
processing path/to/dir: rename /tmp/blahDs3x42aD commands.gen.go: No such file or directory
without some clue of _why_ my program is trying to make that rename happen, what is the semantics, what is the intent. I believe adding that little bit of "cannot finalize file:" really helps.
If these examples don't convince you enough, imagine this error output from a command-line app:
processing path/to/dir: open /some/path/here: No such file or directory
What does that mean? I want to add a reason why the app tried to create a file there (You didn't even know it was a create, not just os.Open! It's ENOENT because an intermediate path doesn't exist.). This is not something that should be added to _every_ error return from this function.
So, what am I missing. Am I "holding it wrong"? Am I supposed to push each of those things into a separate tiny function that all use a defer to wrap all of their errors?
@guybrand Thanks for these numbers. It would be good to have some insights as to why the tryhard
numbers are what they are. Perhaps there's a lot of specific error decoration going on? If so, that's great and if
statements are the right choice.
I'll improve the tool when I get to it.
@tv42 From the examples in your https://github.com/golang/go/issues/32437#issuecomment-503340426, assuming you're not doing it "wrong", it seems like using an if
statement is the way to handle these cases if they all require different responses. try
won't help, and defer
will only make it harder (any other language change proposal in this thread which is trying to make this code simpler to write is so close to the if
statement that it's not worth introducing new mechanism). See also the FAQ of the detailed proposal.
@griesemer Then all I can think of is that you and @rsc disagree. Or that I am, indeed, "doing it wrong", and would like to have a conversation about that.
It is an explicit goal, both back then and today, that any solution make it more likely that users add appropriate context to errors. And we think that try can do that, or we wouldn't have proposed it.
@tv42 @rsc post is about overall error handling structure of good code, which I agree with. If you have an existing piece of code that doesn't fit this pattern exactly and you're happy with the code, leave it alone.
Defers
The primary change from the Gophercon check/handle draft to this proposal was dropping handle
in favor of reusing defer
. Now error context would be added by code like this deferred call (see my earlier comment about error context):
func CopyFile(src, dst string) (err error) {
defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
...
}
The viability of defer as the error annotation mechanism in this example depends on a few things.
_Named error results._ There has been a lot of concern about adding named error results. It is true that we have discouraged that in the past where not needed for documentation purposes, but that is a convention we picked in the absence of any stronger deciding factor. And even in the past, a stronger deciding factor like referring to specific results in the documentation outweighed the general convention for unnamed results. Now there is a second stronger deciding factor, namely wanting to refer to the error in a defer. That seems like it should be no more objectionable than naming results for use in documentation. A number of people have reacted quite negatively to this, and I honestly don't understand why. It almost seems like people are conflating returns without expression lists (so-called “naked returns”) with having named results. It is true that returns without expression lists can lead to confusion in larger functions. Avoiding that confusion by avoiding those returns in long functions often makes sense. Painting named results with the same brush does not.
_Address expressions._ A few people have raised concerns that using this pattern will require Go developers to understand address-of expressions. Storing any value with pointer methods into an interface already requires that, so this does not seem like a significant drawback.
_Defer itself._ A few people have raised concerns about using defer as a language concept at all, again because new users might be unfamiliar with it. Like with address expressions, defer is a core language concept that must be learned eventually. The standard idioms around things like defer f.Close()
and defer l.mu.Unlock()
are so common that it is hard to justify avoiding defer as an obscure corner of the language.
_Performance._ We have discussed for years working on making common defer patterns like a defer at the top of a function have zero overhead compared to inserting that call by hand at each return. We think we know how to do that and will explore it for the next Go release. Even if not, though, the current overhead of approximately 50 ns should not be prohibitive for most calls that need to add error context. And the few performance-sensitive calls can continue to use if statements until defer is faster.
The first three concerns all amount to objections to reusing existing language features. But reusing existing language features is exactly the advance of this proposal over check/handle: there is less to add to the core language, fewer new pieces to learn, and fewer surprising interactions.
Still, we appreciate that using defer this way is new and that we need to give people time to evaluate whether defer works well enough in practice for the error handling idioms they need.
Since we kicked off this discussion last August I've been doing the mental exercise of “how would this code look with check/handle?” and more recently “with try/defer?” each time I write new code. Usually the answer means I write different, better code, with the context added in one place (the defer) instead of at every return or omitted altogether.
Given the idea of using a deferred handler to take action on errors, there are a variety of patterns we could enable with a simple library package. I've filed #32676 to think more about that, but using the package API in that issue our code would look like:
func CopyFile(src, dst string) (err error) {
defer errd.Add(&err, "copy %s %s", src, dst)
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
...
}
If we were debugging CopyFile and wanted to see any returned error and stack trace (similar to wanting to insert a debug print), we could use:
func CopyFile(src, dst string) (err error) {
defer errd.Trace(&err)
defer errd.Add(&err, "copy %s %s", src, dst)
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
...
}
and so on.
Using defer in this way ends up being fairly powerful, and it retains the advantage of check/handle that you can write “do this on any error at all” once at the top of the function and then not worry about it for the rest of the body. This improves readability in much the same way as early quick exits.
Will this work in practice? That's an important open question. We want to find out.
Having done the mental experiment of what defer would look like in my own code for a few months, I think it is likely to work. But of course getting to use it in real code is not always the same. We will need to experiment to find out.
People can experiment with this approach today by continuing to write if err != nil
statements but copying the defer helpers and making use of them as appropriate. If you are inclined to do this, please let us know what you learn.
@tv42, I agree with @griesemer. If you find that additional context is needed to smooth over a connection like the rename being a "finalize" step, there is nothing wrong with using if statements to add additional context. In many functions, however, there is little need for such additional context.
@guybrand, tryhard numbers are great but even better would be descriptions of why specific examples did not convert and furthermore would have been inappropriate to rewrite to be possible to convert. @tv42's example and explanation is an instance of this.
@griesemer about your concern about defer. I was going for that emit
or in the initial proposal handle
. The emit/handle
would be called if the err
is not nil. And will initiate at that moment instead of at the end of the function. The defer gets called at the end. emit/handle
WOULD end the function based on if err
is nil or not. That's why defer wouldn't work.
some data:
out of a ~70k LOC project which I've hawked over to eliminate "naked err returns" religiously, we still have 612 naked error returns. mostly dealing with a case where an error is logged, but the message is only important internally (the message to the user is predefined). try() will have a bigger saving than just two lines per each naked return though, because with predefined errors we can defer a handler and use try in more places.
more interestingly, in the vendor directory, out of ~620k+ LOC, we have only 1600 naked error returns. libraries we choose tend to decorate errors even more religiously than we do.
@rsc if, later, handlers are added to try
will there be an errors/errc package with functions like func Wrap(msg string) func(error) error
so you can do try(f(), errc.Wrap("f failed"))
?
@damienfamed75 Thanks for your explanations. So the emit
will be called when try
finds an error, and it's called with that error. That seems clear enough.
You're also saying that the emit
would end the function if there's an error, and not if the error was handled somehow. If you don't end the function, where does the code continue? Presumably with returning from try
(otherwise I don't understand the emit
that doesn't end the function). Wouldn't it be easier and clearer in that case to just use an if
instead of try
? Using an emit
or handle
would obscure control flow tremendously in those cases, especially because the emit
clause can be in a completely different part (presumably earlier) in the function. (On that note, can one have more than one emit
? If not, why not? What happens if there isn't an emit
? Lots of the same questions that plagued the original check
/handle
draft design.)
Only if one wants to return from a function w/o much extra work besides error decoration, or with always the same work, does it make sense to use try
, and some sort of handler. And that handler mechanism, which runs before a function returns, exists already in defer
.
@guybrand (and @griesemer) with regard to your second unrecognized pattern, see https://github.com/griesemer/tryhard/issues/2
@daved
How will value be derived from the data when much of the public process is driven by sentiment?
Perhaps others may have an experience like mine reported here. I expected to flip through a few instances of try
inserted by tryhard
, find they looked more or less like what already existed in this thread, and move on. Instead, I was surprised to find a case in which try
led to clearly better code, in a way that had not been discussed before.
So there's at least hope. :)
For people trying out tryhard
, if you haven't already, I would encourage you not only to look at what changes the tool made, but also to grep for remaining instances of err != nil
and look at what it left alone, and why.
(And also note that there are a couple of issues and PRs at https://github.com/griesemer/tryhard/.)
@rsc here is my insight as to why I personally don't like the defer HandleFunc(&err, ...)
pattern. It's not because I associate it with naked returns or anything, it just feels too "clever".
There was an error handling proposal a few months (maybe a year?) ago, however I have lost track of it now. I forgot what it was requesting, however someone had responded with something along the lines of:
func myFunction() (i int, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("wrapping the error: %s", err)
}
}()
// ...
return 0, err
// ...
return someInt, nil
}
It was interesting to see to say the least. It was my first time seeing defer
used for error handling, and now it is being shown here. I see it as "clever" and "hacky", and, at least in the example I bring up, it doesn't feel like Go. However, wrapping it in a proper function call with something like fmt.HandleErrorf
does help it feel much nicer. I still feel negatively towards it, though.
Another reason I can see people not liking it is that when one writes return ..., err
, it looks like err
should be returned. But it doesn't get returned, instead the value is modified before sending. I have said before that return
has always seemed like a "sacred" operation in Go, and encouraging code that modifies a returned value before actually returning just feels wrong.
OK, numbers and data it is then. :)
I ran tryhard on the sources several services of our microservice platform, and compared it with the results of loccount and grep 'if err'. I got the following results in the order loccount / grep 'if err' | wc / tryhard:
1382 / 64 / 14
108554 / 66 / 5
58401 / 22 / 5
2052/247/39
12024 / 1655 / 1
Some of our microservices do a lot of error handling and some do only little, but unfortunately, tryhard was only able to automatically improve the code, in at best, 22% of the cases, at worse less than 1%. Now, we are not going to manually rewrite our error handling so a tool like tryhard will be essential to introduce try()
in our codebase. I appreciate that this is a simple preliminary tool, but I was surprised at how rarely it was able to help.
But I think that now, with number in hand, I can say that for our use, try() is not really solving any problem, or, at least not until tryhard becomes much better.
I also found in our code bases that the if err != nil { return err }
use case of try()
is actually very rare, unlike in the go compiler, where it is common. With all due respect, but I think that the Go designers, who are looking at the Go compiler source code far more often than at other code bases, are overestimating the usefulness of try()
because of this.
@beoran tryhard
is very rudimentary at the moment. Do you have a sense for the most common reasons why try
would be rare in your codebase? E.g. because you decorate the errors? Because you do other extra work before returning? Something else?
@rsc , @griesemer
As for examples, I gave two repeating samples here which tryHard missed, one will probably stay as "if Err :=", the other may be resolved
as for error decoration, two recurring patterns I see in the code are (I put the two in one code snippet):
if v, err := someFunction(vars...) ; err != nil {
return fmt.Errorf("extra data to help with where did error occur and params are %s , %d , err : %v",
strParam, intParam, err)
} else if v2, err := passToAnotherFunc(v,vars ...);err != nil {
extraData := DoSomethingAccordingTo(v2,err)
return formatError(err,extraData)
} else {
}
And many the times the formatError is some standard for the app or ever cross repos, most repeating is DbError formatting (one function in all the app/apps, used in dozens of locations), in some cases (without going into "is this a correct pattern") saving some data to log (failing sql query you would not like to pass up the stack) and some other text to the error.
In other words, if I want to "do anything smart with extra data such as logging error A and raising error B, on top of my mention of these two options to extend error handling
This is another option to "more than just return the error and let 'someone else' or 'some other func' handle it"
Which means there is probably more usage for try() in "libraries" than in "executable programs", perhaps I will try to run the Total/Errs/tryHard comparison differentiating libs from runnables ("apps").
I found myself exactly in the situation described in https://github.com/golang/go/issues/32437#issuecomment-503297387
In some level i wrap errors individually, i will not change this with try
, it's fine with if err!=nil
.
At other level i just return err
it's a pain to add the same context for all return, then i will use try
and defer
.
I even already do this with a specif logger that i use in begin of function just in case of error. For me try
and decoration by function is already goish.
@thepudds
If
try
was to hypothetically land in something like Go 1.15, then the very short answer to your question is that someone using Go 1.13
Go 1.13 is not even released yet, so I cannot be using it. And, given my project does not use Go modules, I won't be able to upgrade to Go 1.13. (I believe Go 1.13 will require everyone to use Go modules)
to build code with
try
would see a compile error like this:.\hello.go:3:16: undefined: try note: module requires Go 1.15
(At least as far as I understand what’s been stated about the transition proposal).
That is all hypothetical. It is difficult for me to comment about fictional stuff. And, maybe you like that error, but I find it confusing and unhelpful.
If try is undefined, I would grep for it. And I will find nothing. What should I do then?
And the note: module requires Go 1.15
is the worst help in this situation. Why module
? Why Go 1.15
?
@griesemer
This is perhaps the first suggested language change that affects the feel of the language in more substantial ways. We are aware of that, which is why we kept it so minimal. (I have a hard time imagining the uproar a concrete generics proposal might cause - talking about a language change).
I would rather you spend time on generics, rather then try. Maybe there is a benefit in having generics in Go.
But going back to your point: Programmers get used to how a programming language works and feels. ...
I agree with all your points. But we are talking about replacing particular form of if statement with try function call. This is in the language that prides itself on simplicity and orthogonality. I can get used to everything, but what is the point? To save couple lines of code?
Or, consider the following thought experiment: Imagine Go had no
defer
statement and now somebody makes a strong case fordefer
.defer
doesn't have semantics like anything else in the language, the new language doesn't feel like that pre-defer
Go anymore. Yet, after having lived with it for a decade it seems totally "Go-like".
After many years I still get tricked by defer
body and closed over variables. But defer
pays its price in spades when it comes down to resource management. I cannot imagine Go without defer
. But I am not prepared to pay similar price for try
, because I see no benefits here.
Which is why we are asking people to actually engage with the change by experimenting with it in your own code; i.e., actually writing it, or have
tryhard
run over existing code, and consider the result. I'd recommend to let it sit for a while, perhaps a week or so. Look at it again, and report back.
I tried changing small project of mine (about 1200 lines of code). And it looks similar to your change at https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go I don't see my opinion change about this after a week. My mind is always occupied with something, and I will forget.
... But it's still early, this proposal has only been out for two weeks, ...
And I can see there are 504 messages about this proposal just on this thread already. If I would be interested in pushing this change along, it will take me days if not weeks just to read and comprehend this all. I don't envy your job.
Thank you for taking time to reply to my message. Sorry, if I won't reply to this thread - it just too large for me to monitor, if message is addressed to me or not.
Alex
@griesemer Thanks for the wonderful proposal and tryhard seems to be a more useful that I expect. I will also want to appreciate.
@rsc thanks for the well-articulated response and tool.
Have been following this thread for a while and the following comments by @beoran give me chills
Hiding the error variable and the return does not help to make things easier to understand
Have have had managed several bad written code
before and I can testify it's the worst nightmare for every developer.
The fact the documentation says to use A
likes does not mean it would be followed, the fact remains if it's possible to use AA
, AB
then there is no limit to how it can be used.
To my surprise, people already think the code below is cool
... I think it's an abomination
with all due respect apologies to anyone offended.
parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())
Wait until you check AsCommit
and you see
func AsCommit() error(){
return try(try(try(tail()).find()).auth())
}
The madness goes on and honestly I don't want to believe this is the definition of @robpike simplicity is complicated
(Humor)
Based on @rsc example
// Example 1
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))
// Example 2
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)
// Example 3
try (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
)
Am in favour of Example 2
with a little else
, Please note that this might not be the best approach however
abomination
the others can give birth totry
doesn't behave like a normal function. to give it function-like syntax is little of. go
uses if
and if I can just change it to try tree := r.LookupTree(treeOid) else {
it feels more natural try
& catch
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid) else {
// Heal the world
// I may return with return keyword
// I may not return but set some values to 0
// I may remember I need to log only this
// I may send a mail to let the cute monkeys know the server is on fire
}
Once again I want to apologise for being a little selfish.
@josharian I cannot divulge too much here, however, the reasons are quite diverse. As you say, we do decorate the errors, and or also do different processing, and also, an important use case is that we log them, where the log message differs for each error that a function can return, or because we use the if err := foo() ; err != nil { /* various handling*/ ; return err }
form, or other reasons.
What I want to stress is this: the simple use case for which try()
is designed occur only very rarely in our code base. So, for us there is not much to be gained to adding 'try()' to the language.
EDIT: If try() is going to be implemented then I think the next step should be to make tryhard much better, so it can be used widely to upgrade existing code bases.
@griesemer I will try to address all your concerns one by one from your last response.
First you asked about if the handler doesn't return or exit the function in some way then what would happen. Yes there can be instances where the emit
/handle
clause won't return or exit a function, but rather pick up where it left off. For instance, in the case that we are trying to find a delimiter or something simple using a reader and we reach the EOF
we may not want to return an error when we hit that. So I built this quick example of what that could look like:
func findDelimiter(r io.Reader) ([]byte, error) {
emit err {
// if this doesn't return then continue from where we left off
// at the try function that was called last.
if err != io.EOF {
return nil, err
}
}
bufReader := bufio.NewReader(r)
token := try(bufReader.ReadSlice('|'))
return token, nil
}
Or even could be further simplified to this:
func findDelimiter(r io.Reader) ([]byte, error) {
emit err != io.EOF {
return nil, err
}
bufReader := bufio.NewReader(r)
token := try(bufReader.ReadSlice('|'))
return token, nil
}
Second concern was about disruption of control flow. And yes it would disrupt the flow, but to be fair most of the proposals are somewhat disrupting the flow to have one central error handling function and such. This is no different I believe.
Next, you asked about if we used emit
/handle
more than once in which I say that it's redefined.
If you use emit
more than once it will overwrite the last one and so on. If you do not have any then the try
will have a default handler that just returns nil values and the error. That means that this example here:
func writeStuff(filename string) (io.ReadCloser, error) {
emit err {
return nil, err
}
f := try(os.Open(filename))
try(fmt.Fprintf(f, "stuff\n"))
return f, nil
}
Would do the same thing as this example:
func writeStuff(filename string) (io.ReadCloser, error) {
// when not defining a handler then try's default handler kicks in to
// return nil valued then error as usual.
f := try(os.Open(filename))
try(fmt.Fprintf(f, "stuff\n"))
return f, nil
}
Your last question was about declaring a handler function that is called in a defer
with I assume a reference to an error
. This design doesn't work in the same way that this proposal works in the grounds that a defer
can't immediately stop a function given a condition itself.
I believe I addressed everything in your response and I hope this clears up my proposal just a little bit more. If there are anymore concerns then let me know because I think this whole discussion with everybody is quite fun to ponder new ideas. Keep up the great work everyone!
@velovix, re https://github.com/golang/go/issues/32437#issuecomment-503314834:
Again, this means that
try
punts on reacting to errors, but not on adding context to them. That's a distinction that has alluded me and perhaps others. This makes sense because the way a function adds context to an error is not of particular interest to a reader, but the way a function reacts to errors is important. We should be making the less interesting parts of our code less verbose, and that's whattry
does.
This is a really nice way to put it. Thanks.
@olekukonko, re https://github.com/golang/go/issues/32437#issuecomment-503508478:
To my surprise, people already think the code below is cool
... I thinkit's an abomination
with all due respect apologies to anyone offended.parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()) parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())
Grepping https://swtch.com/try.html, that expression has occurred three times in this thread.
@goodwine brought it up as bad code, I agreed, and @velovix said "despite its ugliness ... is better than what you often see in try-catch languages ... because you can still tell what parts of the code may divert control flow due to an error and which cannot."
No one said it was "cool" or something to put forth as great code. Again, it's always possible to write bad code.
I would also just say re
Errors can be very very expensive, they need as much visibility as possible
Errors in Go are meant to not be expensive. They are everyday, ordinary occurrences and meant to be lightweight. (This is in contrast to some implementations of exceptions in particular. We once had a server that spent far too much of its CPU time preparing and discarding exception objects containing stack traces for failed "file open" calls in a loop checking a list of known locations for a given file.)
@alexbrainman, I am sorry for the confusion about what happens if older versions of Go build code containing try. The short answer is that it is like any other time we change the language: the old compiler will reject the new code with a mostly unhelpful message (in this case "undefined: try"). The message is unhelpful because the old compiler does not know about the new syntax and cannot really be more helpful. People would at that point probably do a web search for "go undefined try" and find out about the new feature.
In @thepudds's example, the code using try has a go.mod that contains the line 'go 1.15', meaning the module's author says the code is written against version of the Go language. This serves as a signal to older go commands to suggest after a compilation error that perhaps the unhelpful message is due to having too old a version of Go. This is explicitly an attempt to make the message a bit more helpful without forcing users to resort to web searches. If it helps, good; if not, web searches seem quite effective anyway.
@guybrand, re https://github.com/golang/go/issues/32437#issuecomment-503287670 and with apologies for likely being too late for your meetup:
One problem in general with functions that return not-quite-error types is that for non-interfaces the conversion to error does not preserve nil-ness. So for example if you have your own custom *MyError concrete type (say, a pointer to a struct) and use err == nil as the signal for success, that's great until you have
func f() (int, *MyError)
func g() (int, error) { x, err := f(); return x, err }
If f returns a nil *MyError, g returns that same value as a non-nil error, which is likely not what was intended. If *MyError is an interface instead of a struct pointer, then the conversion preserves nilness, but even so it's a subtlety.
For try, you might think that since try would only trigger for non-nil values, no problem. For example, this is actually OK as far as returning a non-nil error when f fails, and it is also OK as far as returning a nil error when f succeeds:
func g() (int, error) {
return try(f()), nil
}
So that's actually fine, but then you might see this and think to rewrite it to
func g() (int, error) {
return f()
}
which seems like it should be the same but is not.
There are enough other details of the try proposal that need careful examination and evaluation in real experience that it seemed like deciding about this particular subtlety would be best to postpone.
Thanks everyone for all the feedback so far. At this point, it seems that we have identified the main benefits, concerns, and possible good and bad implications of try
. To make progress, those need to be evaluated further by looking into what try
would mean for actual code bases. The discussion at this point is circling around and repeating those same points.
Experience is now more valuable than continued discussion. We want to encourage people to take time to experiment with what try
would look like in their own code bases and write and link experience reports on the feedback page.
To give everyone some time to breathe and experiment, we are going to pause this conversation and lock the issue for the next week and a half.
The lock will start around 1p PDT/4p EDT (in about 3h from now) to give people a chance to submit a pending post. We will reopen the issue for more discussion on July 1.
Please be assured that we have no intention to rush any new language features without taking the time to understand them well and make sure that they are solving real problems in real code. We will take the time needed to get this right, just as we have done in the past.
That wiki page is crowded with responses to check/handle. I suggest you start a new page.
In any case, I won't have time to continue gardening in the wiki.
@networkimprov, thanks for your help gardening. I created a new top section in https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback. I think that should be better than a whole new page.
I also missed Robert's 1p PDT / 4p EDT note for the lock, so I briefly locked it a bit too early. It's open again, for a little longer.
I was planning on writing this, and just wanted to complete it before it is locked down.
I hope the go team doesn't see the criticism and feel that it is indicative of the majority sentiment. There's always the tendency for the vocal minority to overwhelm the conversation, and I feel like that might have happened here. When everyone is going on a tangent, it discourages others that just want to talk about the proposal AS IS.
So - I will like to articulate my positive position for what it's worth.
I have code that already uses defer for decorating/annotating errors, even for spitting out stack traces, exactly this reason.
See:
https://github.com/ugorji/go-ndb/blob/master/ndb/ndb.go#L331
https://github.com/ugorji/go-serverapp/blob/master/app/baseapp.go#L129
https://github.com/ugorji/go-serverapp/blob/master/app/webrouter.go#L180
which all call errorutil.OnError(*error)
https://github.com/ugorji/go-common/blob/master/errorutil/errors.go#L193
This is along the lines of the defer helpers which Russ/Robert mention earlier.
It is a pattern that I already use, FWIW. It's not magic. It's completely go-like IMHO.
I also use it with named parameters, and it works excellently.
I say this to dispute the notion that anything recommended here is magic.
Secondly, I wanted to add some comments on try(...) as a function.
It has one clear advantage over a keyword, in that it can be extended to take parameters.
There are 2 extension modes that have been discussed here:
For each of them, it is needed that try as a function take a single parameter, and it can be extended later on to take a second parameter if necessary.
The decision has not been made on whether extending try is necessary, and if so, what direction to take. Consequently, the first direction is to provide try to eliminate most of the "if err != nil { return err }" stutter which I have loathed forever but took as the cost of doing business in go.
I personally am glad that try is a function, that I can call inline e.g. I can write
var u User = db.loadUser(try(strconv.Atoi(stringId)))
AS opposed to:
var id int // i have to define this on its own if err is already defined in an enclosing block
id, err = strconv.Atoi(stringId)
if err != nil {
return
}
var u User = db.loadUser(id)
As you can see, I just took 6 lines down to 1. And 5 of those lines are truly boilerplate.
This is something I have dealt with many times, and I have written a lot of go code and packages - you can check my github to see some of the ones I have posted online, or my go-codec library.
Finally, a lot of the comments in here haven't truly shown problems with the proposal, as much as they have posited their own preferred way to solving the problem.
I personally am thrilled that try(...) is coming in. And I appreciate the reasons why try as a function is the preferred solution. I clearly like that defer is being used here, as it only just makes sense.
Let's remember one of go's core principles - orthogonal concepts that can be combined well. This proposal leverages a bunch of go's orthogonal concepts (defer, named return parameters, built-in functions to do what is not possible via user code, etc) to provide the key benefit that
go users have universally requested for years i.e. reducing/eliminating the if err != nil { return err } boilerplate. The Go User Surveys show that this is a real issue. The go team is aware that it is a real issue. I am glad that the loud voices of a few are not skewing the position of the go team too much.
I had one question about try as an implicit goto if err != nil.
If we decide that is the direction, will it be hard to retrofit "try does a return" to "try does a goto",
given that goto has defined semantics that you cannot go past unallocated variables?
Thanks for your note, @ugorji.
I had one question about try as an implicit goto if err != nil.
If we decide that is the direction, will it be hard to retrofit "try does a return" to "try does a goto",
given that goto has defined semantics that you cannot go past unallocated variables?
Yes, exactly right. There is some discussion on #26058.
I think 'try-goto' has at least three strikes against it:
(1) you have to answer unallocated variables,
(2) you lose stack information about which try failed, which in contrast you can still capture in the return+defer case, and
(3) everyone loves to hate on goto.
Yep, try
is the way to go.
I've tried to add try
once, and I liked it.
Patch - https://github.com/ascheglov/go/pull/1
Topic on Reddit - https://www.reddit.com/r/golang/comments/6vt3el/the_try_keyword_proofofconcept/
@griesemer
Continuing from https://github.com/golang/go/issues/32825#issuecomment-507120860 ...
Going along with the premise that abuse of try
will be mitigated by code review, vetting, and/or community standards, I can see the wisdom in avoiding changing the language in order to restrict the flexibility of try
. I don't see the wisdom of providing additional facilities that strongly encourage the more difficult/unpleasant to consume manifestations.
In breaking this down some, there seem to be two forms of error path control flow being expressed: Manual, and Automatic. Regarding error wrapping, there seem to be three forms being expressed: Direct, Indirect, and Pass-through. This results in six total "modes" of error handling.
Manual Direct, and Automatic Direct modes seem agreeable:
wrap := func(err error) error {
return fmt.Errorf("failed to process %s: %v", filename, err)
}
f, err := os.Open(filename)
if err != nil {
return nil, wrap(err)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, wrap(err)
}
// in errors, named better, and optimized
WrapfFunc := func(format string, args ...interface{}) func(error) error {
return func(err error) error {
if err == nil {
return nil
}
s := fmt.Sprintf(format, args...)
return errors.Errorf(s+": %w", err)
}
}
```go
wrap := errors.WrapfFunc("failed to process %s", filename)
f, err := os.Open(filename)
try(wrap(err))
defer f.Close()
info, err := f.Stat()
try(wrap(err))
Manual Pass-through, and Automatic Pass-through modes are also simple enough to be agreeable (despite often being a code smell):
```go
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, err
}
f := try(os.Open(filename))
defer f.Close()
info := try(f.Stat())
However, Manual Indirect and Automatic Indirect modes are both quite disagreeable due to the high likelihood of subtle mistakes:
defer errd.Wrap(&err, "failed to do X for %s", filename)
var f *os.File
f, err = os.Open(filename)
if err != nil {
return
}
defer f.Close()
var info os.FileInfo
info, err = f.Stat()
if err != nil {
return
}
defer errd.Wrap(&err, "failed to do X for %s", filename)
f := try(os.Open(filename))
defer f.Close()
info := try(f.Stat())
Again, I can understand not forbidding them, but facilitating/blessing the indirect modes is where this is still raising clear red flags for me. Enough so, at this time, for me to remain emphatically skeptical of the entire premise.
Try must not be a function to avoid that damned
info := try(try(os.Open(filename)).Stat())
file leak.
I mean try
statement will not allow chaining. And it is better looking as a bonus. There are compatibility issues though.
@sirkon Since try
is special, the language could disallow nested try
's if that's important - even if try
looks like a function. Again, if this is the only road block for try
, that could be easily addressed in various ways (go vet
, or language restriction). Let's move on from this - we've heard it many times now. Thanks.
Let's move on from this - we've heard it many times before
“This is so boring, let’s move on from this”
There is another good analogue:
- Your theory contradicts the facts!
- The worse for the facts!
By Hegel
I mean you are solving a problem that doesn’t exists in fact. And the ugly way at that.
Let’s take a look at where this problem actually appears: handling side effects from the outer world, that’s it. And this actually is one of the easiest part logically in software engineering. And the most important at that. I cannot understand why do we need a simplification for the easiest thing which will cost us lesser reliability.
IMO the hardest problem of such kind is data consistency preservation in distributed systems (and not so distributed in fact). And error handling was not a problem I was fighting with in Go when solving these. Lack of slice and map comprehensions, lack of sum/algebraic/variance/whatever types was FAR more annoying.
Since the debate here seems to continue unabated, let me repeat again:
Experience is now more valuable than continued discussion. We want to encourage people to take time to experiment with what
try
would look like in their own code bases and write and link experience reports on the feedback page.
If concrete experience provides significant evidence for or against this proposal, we'd like to hear that here. Personal pet peeves, hypothetical scenarios, alternative designs, etc. we can acknowledge but they are less actionable.
Thanks.
I don't want to be rude here, and I appreciate all your moderation, but the community has spoken extremely strongly about error handling being changed. Changing things, or adding new code, will upset _all_ the people who prefer the current system. You can't make everyone happy, so let's focus on the 88% we can make happy (number derived from the vote ratio below).
At the time of this writing, the "leave it alone" thread is at 1322 votes up and 158 down. This thread is at 158 up and 255 down. If that isn't a direct end of this thread on error handling, then we should have a very good reason to keep pushing the issue.
It is possible to always do what your community screams for and to destroy your product at the same exact time.
At a minimum, I think this specific proposal should be considered as failed.
Luckily, go
is not designed by committee. We need to trust that the custodians of the language we all love will continue to make the best decision given all the data available to them, and will not make a decision based on popular opinion of the masses. Remember - they use go also, just like us. They feel the pain points, just like us.
If you have a position, take the time to defend it like the way the Go team defends their proposals. Else you are just drowning the conversation with fly-by-night sentiments that are not actionable and do not carry the conversations forward. And it makes it harder for folks that want to engage, as said folks may just want to wait it out until the noise dies down.
When the proposal process started, Russ made a big deal about evangelizing the need for experience reports as a way to influence a proposal or make your request heard. Let's at least try to honor that.
The go team has been taking all actionable feedback into consideration. They haven't failed us yet. See the detailed documents produced for alias, for modules, etc. Let's at least give them the same regard and spend time to think through our objections, respond to their position on your objections, and make it harder for your objection to be ignored.
Go's benefit has always been that it is a small, simple language with orthogonal constructs designed by a small group of folks who would think through the space critically before committing to a position. Let's help them where we can, instead of just saying "see, popular vote says no" - where many folks voting may not even have much experience in go or understand go fully. I've read serial posters who admitted that they do not know some foundational concepts of this admittedly small and simple language. That makes it hard to take your feedback seriously.
Anyway, sucks that I'm doing this here - feel free to remove this comment. I will not be offended. But someone has to say this bluntly!
This whole 2nd proposal thing looks very similar to digital influencers organizing a rally to me. Popularity contests do not evaluate technical merits.
People may be silent but they still expect Go 2. I personally look forward to this and the rest of Go 2. Go 1 is a great language and well suited to different kinds of programs. I hope Go 2 will expand that.
Finally I will also reverse my preference for having try
as an statement. Now I support the proposal as it is. After so many years under the "Go 1" compat promise people think Go has been carved in stone. Due to that problematic assumption, not changing the language syntax in this instance seems like a much better compromise in my eyes now. Edit: I also look forward to seeing the experience reports for fact-checking.
PS: I wonder what kind of opposition will happen when generics are proposed.
We have around a dozen tools written in a go at our company. I ran tryhard tool against our codebase and found 933 potential try() candidates. Personally, I believe try() function is a brilliant idea because it solves more than just code boilerplate issue.
It enforces both the caller and called function/method to return the error as the last parameter. This will not be allowed:
var file= try(parse())
func parse()(err, result) {
}
It enforces one way to deal with errors instead declaring the error variable and loosely allowing err!=nil err==nil pattern, which hinders readability, increases risk of error-prone code in IMO:
func Foo() (err error) {
var file, ferr = os.Open("file1.txt")
if ferr == nil {
defer file.Close()
var parsed, perr = parseFile(file)
if perr != nil {
return
}
fmt.Printf("%s", parsed)
}
return nil
}
With try(), code is more readable, consistent and safer in my opinion:
func Foo() (err error) {
var file = try(os.Open("file.txt"))
defer file.Close()
var parsed = try(parseFile(file))
fmt.Printf(parsed)
return
}
I ran some experiments similar to what @lpar did on all of Heroku's unarchived Go repositories (public and private).
The results are in this gist: https://gist.github.com/freeformz/55abbe5da61a28ab94dbb662bfc7f763
cc @davecheney
@ubikenobi Your safer function ~is~ was leaking.
Also, I've never seen a value returned after an error. Though, I could imagine it making sense when a function is all about the error and the other values returned are not contingent on the error itself (maybe leading to two error returns with the second "guarding" the previous values).
Last, while not common, err == nil
provides a legitimate test for some early returns.
@Daved
Thanks for pointing out about leak, I forgot to add defer.Close() on both examples. (updated now).
I rarely see err return in that order too but it is still good to be able to catch those at compile time if they are mistakes than by design.
I see err==nil case as an exception than a norm in most cases. It can be useful in some cases as you mentioned but what I don't like is developers choosing inconsistently without a valid reason. Luckily, in our codebase vast majority of statements are err!=nil, which can easily benefit from try() function.
tryhard
against a large Go API which I maintain with a team of four other engineers full time. In 45580 lines of Go code, tryhard
identified 301 errors to rewrite (so, it would be a +301/-903 change), or would rewrite about 2% of the code assuming each error takes approximately 3 lines. Taking into account comments, whitespace, imports, etc. that feels substantial to me.try
would change my work, and subjectively it flows very nicely to me! The verb try feels clearer to me that something could go wrong in the calling function, and accomplishes it compactly. I'm very used to writing if err != nil
, and I don't really mind, but wouldn't mind changing, either. Writing and refactoring the empty variable preceding the error (i.e. making the empty slice/map/variable to return) repetitively is probably a more tedious than the err
itself. try
was variadic if you wanted to optionally add context like try(json.Unmarshal(b, &accountBalance), "failed to decode bank account info for user %s", user)
. Edit: this point probably off topic; from looking at non-try rewrites this is where this happens, though.Shouldn't this be done on source that has been vetted by experienced Gophers to ensure that the replacements are rational? How much of that "2%" rewrite should have been rewritten with explicit handling? If we do not know that, then LOC remains a relatively useless metric.
*Which is exactly why my post earlier this morning focused on "modes" of error handling. It's easier and more substantive to discuss the modes of error handling try
facilitates and then wrestle with potential hazards of the code we are likely to write than it is to run a rather arbitrary line counter.
@kingishb How many of found _try_ spots are in public functions from non-main packages? Typically public functions should return package-native (i.e. wrapped or decorated) errors....
@networkimprov That's an overly simplistic formula for my sensibilities. Where that rings true is in terms of API surfaces returning inspectable errors. It is normally appropriate to add context to an error message based on the relevance of the context, not it's position in the call stack.
Many false positives are likely making it through in the current metrics. And what about misses that occur due to following suggested practices (https://blog.golang.org/errors-are-values)? try
would likely reduce usage of such practices and, in that sense, they are prime targets for replacement (probably one of the only use cases really intriguing to me). So, again, this seems pointless to scrape existing source without much more due diligence.
Thanks @ubikenobi, @freeformz, and @kingishb for collecting your data, much appreciated! As an aside, if you run tryhard
with the option -err=""
if will also try to work with code where the error variable is called something else than err
(such as e
). This may yield a few more cases, depending on the code base (but also possibly increase the chance of false positives).
@griesemer in case you are looking for more data points. I've ran tryhard
against two of our micro-services, with these results:
cloc v 1.82 / tryhard
13280 Go code lines / 148 identified for try (1%)
Another service:
9768 Go code lines / 50 identified for try (0.5%)
Subsequently tryhard
inspected a wider set of various micro-services:
314343 Go code lines / 1563 identified for try (0.5%)
Doing a quick inspection. The types of packages that try
could optimise are typically adapters/service wrappers that transparently return the (GRPC) error returned from the wrapped service.
Hope this helps.
It's an absolutely bad idea.
err
var appear for defer
? What about "explicit better than implicit"?defer
will create a lot of ugly and hard-to-understand code. os.Exit
, your errors will be unchecked.I just ran tryhard
on a package (with vendor) and it reported 2478
with the code count dropping from 873934
to 851178
but I'm not sure how to interpret that because I don't know how much of that is due to over-wrapping (with the stdlib lacking support for stack-trace error wrapping) or how much of that code is even about error handling.
What I do know, however, is that just this week alone I wasted an embarrassing amount of time due to copy-pasta like if err != nil { return nil }
and errors that look like error: cannot process ....file: cannot parse ...file: cannot open ...file
.
\
I also wouldn't take the attempts at appeal-to-authority too seriously either, because these same authorities are known to reject new ideas and proposals even after their own ignorance and/or misunderstanding is pointed out.
\
We ran tryhard -err=""
on our biggest (±163k lines of code including tests) service - it has found 566 occurrences. I suspect it would be even more in practice, since some of the code was written with if err != nil
in mind, so it was designed around it (Rob Pike's "errors are values" article on avoiding repeating comes to mind).
@griesemer I added a new file to the gist. It was generated with -err="". I spot checked and there are a few changes. I also updated tryhard this morning as well, so the newer version was also used.
@griesemer I think tryhard would be more useful if it could tally:
a) the number of call sites yielding an error
b) the number of single-statement if err != nil [&& ...]
handlers (candidates for on err
#32611)
c) the number of those which return anything (candidates for defer
#32676)
d) the number of those which return err
(candidates for try()
)
e) the number of those which are in exported functions of non-main packages (likely false positive)
Comparing total LoC to instances of return err
sorta lacks context, IMO.
@networkimprov Agreed - similar suggestions have been brought up before. I'll try to find some time over the next days to improve this.
Here are the statistics of running tryhard over our internal codebase (only our code, not dependencies):
Before:
After tryhard:
Edit: Now that @griesemer updated tryhard to include summary statistics, here are a couple more:
if
statements are if <err> != nil
try
candidatesLooking through the replacements that tryhard found, there are certainly types of code where the usage of try
would be very prevalent, and other types where it would be rarely used.
I also noticed some places that tryhard could not transform, but would benefit greatly from try. For instance, here is some code we have for decoding messages according to a simple wire protocol (edited for simplicity/clarity):
func (req *Request) Decode(r Reader) error {
typ, err := readByte(r)
if err != nil {
return err
}
req.Type = typ
req.Body, err = readString(r)
if err != nil {
return unexpected(err)
}
req.ID, err = readID(r)
if err != nil {
return unexpected(err)
}
n, err := binary.ReadUvarint(r)
if err != nil {
return unexpected(err)
}
req.SubIDs = make([]ID, n)
for i := range req.SubIDs {
req.SubIDs[i], err = readID(r)
if err != nil {
return unexpected(err)
}
}
return nil
}
// unexpected turns any io.EOF into an io.ErrUnexpectedEOF.
func unexpected(err error) error {
if err == io.EOF {
return io.ErrUnexpectedEOF
}
return err
}
Without try
, we just wrote unexpected
at the return points where it's needed since there's no great improvement by handling it in one place. However, with try
, we can apply the unexpected
error transformation with a defer and then dramatically shorten the code, making it clearer and easier to skim:
func (req *Request) Decode(r Reader) (err error) {
defer func() { err = unexpected(err) }()
req.Type = try(readByte(r))
req.Body = try(readString(r))
req.ID = try(readID(r))
n := try(binary.ReadUvarint(r))
req.SubIDs = make([]ID, n)
for i := range req.SubIDs {
req.SubIDs[i] = try(readID(r))
}
return nil
}
@cespare Fantastic report!
The fully reduced snippet is generally better, but the parenthesis are even worse than I have expected, and the try
within the loop is as bad as I have expected.
A keyword is far more readable and it's a bit surreal that that is a point many others differ on. The following is readable and does not make me concerned about subtleties due to only one value being returned (though, it still could come up in longer functions and/or those with much nesting):
func (req *Request) Decode(r Reader) (err error) {
defer func() { err = wrapEOF(err) }()
req.Type = try readByte(r)
req.Body = try readString(r)
req.ID = try readID(r)
n := try binary.ReadUvarint(r)
req.SubIDs = make([]ID, n)
for i := range req.SubIDs {
req.SubIDs[i], err = readID(r)
try err
}
return nil
}
*Being fair about it, code highlighting would help a lot, but that just seems like cheap lipstick.
Do you understand that the most advantage you get in case of really bad code?
If you use unexpected()
or return error as is, you know nothing about your code and your application.
try
can't help you write better code, but can produce more bad code.
@cespare A decoder can also be a struct with an error type inside it, with the methods checking for err == nil
before every operation and returning a boolean ok.
Because this is the process we use for codecs, try
is absolutely useless because one can easily make a non magic, shorter, and more succinct idiom for handling errors for this specific case.
@makhov By "really bad code", I assume you mean code that doesn't wrap errors.
If so, then you can take code that looks like this:
a, b, c, err := someFn()
if err != nil {
return ..., errors.Wrap(err, ...)
}
And turn it into semantically identical[1] code that looks like this:
a, b, c, err := someFn()
try(errors.Wrap(err, ...))
The proposal is not saying you must use defer for error wrapping, only explaining why the handle
keyword of the previous iteration of the proposal is not necessary, as it can be implemented in terms of defer without any language changes.
(Your other comment also seems to be based on examples or pseudo-code in the proposal, as opposed to the core of what is being proposed)
I ran tryhard
on my codebase with 54K LOC, 1116 instances were found.
I saw the diff, and I have to say that I have so very little construct that would greatly benefit from try, because almost my entire use of if err != nil
type of construct is a simple single-level block that just returns the error with added context. I think I only found a couple of instances where try
would actually change the construct of the code.
In other words, my take is that try
in its current form gives me:
- **if err := **json.NewEncoder(&buf).Encode(in)**; err != nil {**
- **return err**
- **}**
+ try(json.NewEncoder(&buf).Encode(in))
while it introduces these problems for me:
As I wrote earlier in this thread, I can live with try
, but after trying it out on my code I think I'd personally rather not have this introduced to the language. my $.02
useless feature,it save typing, but not a big deal.
I rather choose the old way.
write more error handler make to program easy to trouble shooting.
Just some thoughts...
That idiom is useful in go but it is just that: an idiom that you must
teach to newcomers. A new go programmer has to learn that, otherwise they
may be even tempted to refactor out the "hidden" error handling. Also, the
code's not shorter using that idiom (quite the opposite) unless you forget
to count the methods.
Now let's imagine try is implemented, how useful will that idiom be for
that use case? Considering:
So maybe that idiom will be considered superseded by try.
Em ter, 2 de jul de 2019 18:06, as notifications@github.com escreveu:
@cespare https://github.com/cespare A decoder can also be a struct with
an error type inside it, with the methods checking for err == nil before
every operation and returning a boolean ok.Because this is the process we use for codecs, try is absolutely useless
because one can easily make a non magic, shorter, and more succinct idiom
for handling errors for this specific case.—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAT5WM3YDDRZXVXOLDQXKH3P5O7L5A5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODZCRHXA#issuecomment-507843548,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAT5WMYXLLO74CIM6H4Y2RLP5O7L5ANCNFSM4HTGCZ7Q
.
Verbosity in error handling is a good thing in my opinion. In other words I don't see a strong use case for try.
I'm open to this idea but I feel that it should include some mechanism to determine where the execution split happened. Xerror/Is would be fine for some cases (e.g. if the error is an ErrNotExists you can infer it happened on an Open), but for others - including legacy errors in libraries - there's no substitute.
Could a built-in similar to recover possibly be included to provide context information on where the control flow changed? Possibly, to keep it cheap, a separate function used in place of try().
Or perhaps a debug.Try with the same syntax as try() but with the added debug information? This way try() could be just as useful with code using old errors, without forcing you to resort to old error handling.
The alternative would be for try() to wrap and add context but in most cases this would reduce performance for no purpose, hence the suggestion of additional functions.
Edit: after writing this it occurred to me that the compiler could determine which variant of try() to use based on whether any defer statements use this context-providing function similar to "recover". Not certain on the complexity of this though
@lestrrat I would not say my opinion in this comment but if there is a chance to explain you how "try" may affect good for us, it would be that two or more tokens can be written in if statement. So if you write 200 conditions in a if statement, you will be able to reduce many lines.
if try(foo()) == 1 && try(bar()) == 2 {
// err
}
n1, err := foo()
if err != nil {
// err
}
n2, err := bar()
if err != nil {
// err
}
if n1 == 1 && n2 == 2 {
// err
}
@mattn that's the thing though, _theoretically_ you are absolutely correct. I'm sure we can come up with cases where try
would fit just wonderfully.
I just supplied data that in real life, at least _I_ found almost no occurrence of such constructs that would benefit from the translation to try in _my code_.
It's possible that I write code differently from the rest of the World, but I just thought it worth it for somebody to chime in that, based on PoC translation, that some of us don't actually gain much from the introduction of try
into the language.
As an aside, I still wouldn't use your style in my code. I'd write it as
n1 := try(foo())
n2 := try(bar())
if n1 == 1 && n2 == 2 {
return errors.New(`boo`)
}
so I still would be saving about the same amount of typing per instance of those n1/n2/....n(n)s
Why have a keyword (or function) at all?
If the calling context expects n+1 values, then all is as before.
If the calling context expects n values, the try behaviour kicks in.
(This is particularly useful in the case n=1 which is where all the awful clutter comes from.)
My ide already highlights ignored return values; it would be trivial to offer visual cues for this if required.
@balasanjay Yes, wrapping errors is the case. But also we have logging, different reactions on different errors (what we should do with error variables, e.g. sql.NoRows
?), readable code and so on. We write defer f.Close()
immediately after opening a file to make it clear for readers. We check errors immediately for the same reason.
Most importantly, this proposal violates the rule "errors are values". This is how Go is designed. And this proposal goes directly against the rule.
try(errors.Wrap(err, ...))
is another piece of terrible code because it contradicts both this proposal and the current Go design.
I tend to agree with @lestrrat
As usually foo() and bar() are actually:
SomeFunctionWithGoodName(Parm1, Parms2)
then the suggested @mattn syntax would actually be:
if try(SomeFunctionWithGoodName(Parm1, Parms2)) == 1 && try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) == 2 {
}
Readability will usually be a mess .
consider a return value:
someRetVal, err := SomeFunctionWithGoodName(Parm1, Parms2)
is in use more often than just comparing to a const such as 1 or 2 and it not gets worse but requires a double assignment feature:
if a := try(SomeFunctionWithGoodName(Parm1, Parms2)) && b:= try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) {
}
As for all the use cases ("how much did tryhard help me"):
$find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
@makhov
this proposal violates the rule "errors are values"
Not really. Errors are still values in this proposal. try()
is just simplifying the control flow by being a shortcut for if err != nil { return ...,err }
. The error
type is already somehow "special" by being a built-in interface type. This proposal is just adding a built-in function that complements the error
type. There is nothing extraordinary here.
@ngrilly Simplifying? How?
func (req *Request) Decode(r Reader) error {
defer func() { err = unexpected(err) }()
req.Type = try(readByte(r))
req.Body = try(readString(r))
req.ID = try(readID(r))
n := try(binary.ReadUvarint(r))
req.SubIDs = make([]ID, n)
for i := range req.SubIDs {
req.SubIDs[i] = try(readID(r))
}
return nil
}
How I should understand that error was returned inside loop? Why it's assigned to err
var, not to foo
?
Is it simpler to keep it in mind and not keep it in code?
@daved
the parenthesis are even worse than I have expected [...] A keyword is far more readable and it's a bit surreal that that is a point many others differ on.
Choosing between a keyword and a built-in function is mostly an aesthetic and syntactic issue. I honestly don't understand why this is so important to your eyes.
PS: The built-in function has the advantage of being backward compatible, being extensible with other parameters in the future, and avoiding the issues around operator precedence. The keyword has the advantage of... being a keyword, and signaling try
is "special".
@makhov
Simplifying?
Ok. The right word is "shortening".
try()
shortens our code by replacing the pattern if err != nil { return ..., err }
by a call to the built-in try()
function.
It's exactly like when you identify a recurring pattern in your code, and you extract it in a new function.
We already have built-in functions like append(), that we could replace by writing the code "in extenso" ourselves each time we need to append something to a slice. But because we do it all the time, it was integrated in the language. try()
is not different.
How I should understand that error was returned inside loop?
The try()
in the loop acts exactly like the try()
in the rest of the function, outside of the loop. If readID()
returns an error, then the function returns the error (after having decorated if).
Why it's assigned to err var, not to foo?
I see no foo
variable in your code example...
@makhov I think that the snippet is incomplete as the returned error is not named (I quickly re-read the proposal but couldnt see if the variable name err
is the default name if none is set).
Having to rename returned parameters is one of the point that people who reject this proposal do not like.
func (req *Request) Decode(r Reader) (err error) {
defer func() { err = unexpected(err) }()
req.Type = try(readByte(r))
req.Body = try(readString(r))
req.ID = try(readID(r))
n := try(binary.ReadUvarint(r))
req.SubIDs = make([]ID, n)
for i := range req.SubIDs {
req.SubIDs[i] = try(readID(r))
}
return nil
}
@pierrec maybe we could have a function like recover()
to retrieve the error if not in named parameter ?
defer func() {err = unexpected(tryError())}
@makhov You can make it more explicit:
func (req *Request) Decode(r Reader) error {
req.Type, err := readByte(r)
try(err) // or add annotation like try(annotate(err, ...))
req.Body, err := readString(r)
try(err)
req.ID, err := readID(r)
try(err)
n, err := binary.ReadUvarint(r)
try(err)
req.SubIDs = make([]ID, n)
for i := range req.SubIDs {
req.SubIDs[i], err := readID(r)
try(err)
}
return nil
}
@pierrec Ok, let's change it:
func (req *Request) Decode(r Reader) error {
var errOne, errTwo error
defer func() { err = unexpected(???) }()
req.Type = try(readByte(r))
…
}
@reusee And why is it better than this?
func (req *Request) Decode(r Reader) error {
req.Type, err := readByte(r)
if err != nil { return err }
…
}
In what moment did we all decide that shortness better than readability?
@flibustenet Thank you for understanding the problem. It looks much better but I'm still not sure that we need broken backward compatibility for this small "improvement". It's very annoying if I have an application that stops building on the new version of Go:
package main
func main() {
// ...
try("a", "b")
// ...
}
func try(a, b string) {
// ...
}
@makhov I agree that this needs to be clarified: does the compiler errors out when it cannot figure out the variable? I thought it would.
Maybe the proposal needs to clarify this point? Or did I miss it in the document?
@flibustenet yes that's one way of using try() but it seems to me that it's not an idiomatic way of using try.
@cespare From what you wrote it seems that modification of the return values in defer is a feature of try
but you can already do this.
https://play.golang.com/p/ZMauFmt9ezJ
(Sorry if I misinterpreted what you said)
@jan-g Regarding https://github.com/golang/go/issues/32437#issuecomment-507961463: The idea of invisibly handling errors has come up multiple times. The problem with such an implicit approach is adding an error return to a called function may cause the calling function to silently and invisibly behave differently. We absolutely want to be explicit when errors are checked. An implicit approach also flies against the general principle in Go that everything is explicit.
@griesemer
I tried tryhand
on one of my projects(https://github.com/komuw/meli) and it didn't make any change.
gobin github.com/griesemer/tryhard
Installed github.com/griesemer/[email protected] to ~/go/bin/tryhard
```bash
~/go/bin/tryhard -err "" -r
0
most of my err handling looks like;
```Go
import "github.com/pkg/errors"
func CreateDockerVolume(volName string) (string, error) {
volume, err := VolumeCreate(volName)
if err != nil {
return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
}
return volume.Name, nil
}
@komuw First of all, make sure to provide a filename or directory argument to tryhard
, as in
tryhard -err="" -r . // <<< note the dot
tryhard -err="" -r filename
Also, code like you have in your comment won't be rewritten as it does specific error handling in the if
block. Please read the documentation of tryhard
as to when it applies. Thanks.
func CreateDockerVolume(volName string) (string, error) {
volume, err := VolumeCreate(volName)
if err != nil {
return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
}
return volume.Name, nil
}
This is a somewhat interesting example. My first reaction when looking at it was to ask whether this would produce stuttering error strings like:
unable to create docker volume: VolumeName: could not create volume VolumeName: actual problem
The answer is that it doesn't, because the VolumeCreate
function (from a different repo) is:
func (cli *Client) VolumeCreate(ctx context.Context, options volumetypes.VolumeCreateBody) (types.Volume, error) {
var volume types.Volume
resp, err := cli.post(ctx, "/volumes/create", nil, options, nil)
defer ensureReaderClosed(resp)
if err != nil {
return volume, err
}
err = json.NewDecoder(resp.body).Decode(&volume)
return volume, err
}
In other words, the additional decoration on the error is useful because the underlying function didn't decorate its error. That underlying function can be slightly simplified with try
.
Perhaps the VolumeCreate
function really should be decorating its errors. In that case, however, it's not clear to me that the CreateDockerVolume
function should add additional decoration, since it has no new information to provide.
@neild
Even if VolumeCreate
would decorate the errors, we would still need CreateDockerVolume
to add its decoration, as VolumeCreate
may be called from various other functions, and if something fails (and hopefully logged) you would like to know what failed - which in this case is CreateDockerVolume
,
Nevertheless, Considering VolumeCreate
is a part of the APIclient interface.
The same goes for other libraries - os.Open
can well decorate the file name, reason for error etc, but
func ReadConfigFile(...
func WriteDataFile(...
etc - calling os.Open
are the actual failing parts you would like to see in order to log, trace, and handle your errors - especially, but not only in production env.
@neild thanks.
I don't want to derail this thread, but...
Perhaps the VolumeCreate function really should be decorating its errors.
In that case, however, it's not clear to me that the
CreateDockerVolume function
should add additional decoration,
The problem is, as the author of the CreateDockerVolume
function i may not
know whether the author of VolumeCreate
had decorated their errors so i
don't need to decorate mine.
And even if i knew that they had, they could decide to un-decorate their
function at a later version. And since that change is not api changing they
would release it as a patch/minor version and now my function which was
dependent on their function having decorated errors does not have all the
info i need.
So generally i find myself decorating/wrapping even if the library I'm
calling has already wrapped.
I had a thought while talking about try
with a co-worker. Maybe try
should only be enabled for the standard library in 1.14. @crawshaw and @jimmyfrasche both did a quick tour through some cases and gave some perspective, but actually re-writing the standard library code using try
as much as possible would be valuable.
That gives the Go team time to re-write a non-trivial project using it, and the community can have an experience report on how it works out. We'd know how often it's used, how often it needs to be paired with a defer
, if it changes the readability of the code, how useful tryhard
is, etc.
It's a bit against the spirit of the standard library, allowing it to use something that regular Go code can't, but it does give us a playground to see how try
affects an existing codebase.
Apologies if someone else has thought of this already; I went through the various discussions and didn't see a similar proposal.
@jonbodner https://go-review.googlesource.com/c/go/+/182717 gives you a pretty good idea of what that might look like.
And I forgot to say: I participated in your survey and I voted for better error handling, not this.
I meant I would like to see stricter impossible to forget error processing.
@jonbodner https://go-review.googlesource.com/c/go/+/182717 gives you a pretty good idea of what that might look like.
To sum up:
if ... { return err }
)About 6,000 replacements in total of what appears to be just a cosmetic change: will not expose existing errors, perhaps will not introduce new ones (correct me if I am wrong about either.)
Would I, in a capacity of a maintainer, bother to do something like this with my own code? Not unless I write the replacement tool myself. Which makes it all right for golang/go
repository.
PS An interesting disclaimer in CL:
... Some transformations
may be incorrect
due to the limitations of the
tool (see https://github.com/griesemer/tryhard)...
Like xerrors
, how about taking the first step to use it as a third party package?
For example, try using the package below.
https://github.com/junpayment/gotry
However, I think try itself is a great idea, so I think that there is also an approach that actually uses it with less influence.
===
As an aside, there are two things I'm concerned about try.
1.There is an opinion that the line can be omitted, but it seems that there is no consideration of the defer(or handler) clause.
For example, when error handling is in detail.
foo, err: = Foo ()
if err! = nil {
if err.Error () = "AAA" {
some action for AAA
} else if err.Error () = "BBB" {
some action for BBB
} else if err.Error () = "CCC" {
some action for CCC
} else {
return err
}
}
If you simply replace this with try, it will be as follows.
handler: = func (err error) {
if err.Error () = "AAA" {
some action for AAA
} else if err.Error () = "BBB" {
some action for BBB
} else if err.Error () = "CCC" {
some action for CCC
} else {
return err
}
}
foo: = try (Foo (), handler)
2.There may be other bad packages that accidentally implemented the error interface.
type Bad struct {}
func (bad * Bad) Error () {
return "i really do not intend to be an error"
}
@junpayment Thanks for your gotry
package - I guess that's one way for getting a bit of a feel for try
but it will be a bit annoying to have to type-assert all Try
results out of an interface{}
in actual use.
Regarding your two questions:
1) I'm not sure where you're going with this. Are you suggesting try
should accept a handler as in your example? (and as we had in an earlier internal version of try
?)
2) I'm not too worried about functions accidentally implementing the error interface. This problem is not new and doesn't seem to have caused serious issues as far as we know.
@jonbodner https://go-review.googlesource.com/c/go/+/182717 gives you a pretty good idea of what that might look like.
Thanks for doing this exercise. However, this confirms to me what I suspected, the go source code itself has a lot of places where try()
would be useful because the error is just passed on. However, as I can see from the experiments with tryhard
which others and myself submitted above, for many other codebases try()
would not be very useful because in application code errors tend to be actually handled, not just passed on.
I think that is something the Go designers should keep in mind, the go compiler and run time are somewhat "unique" Go code, different from Go application code. Therefore, I think try()
should be enhanced to also be useful in other cases where the error actually has to be handled, and where doing error handling with a defer statement isn't really desirable.
@griesemer
it will be a bit annoying to have to type-assert all Try results out of an interface{} in actual use.
You're right. This method requires the caller to cast the type.
I'm not sure where you're going with this. Are you suggesting try should accept a handler as in your example? (and as we had in an earlier internal version of try?)
I made a mistake. Should have been explained using defer rather than handler. I'm sorry.
What I wanted to say is that there is a case where it does not contribute to the amount of code as a result of the error handling process which is omitted need to be described in the defer anyway.
The impact is expected to be more pronounced when you want to handle errors in detail.
So, rather than reducing the number of lines of code, we can understand proposal, which organizes error handling locations.
I'm not too worried about functions accidentally implementing the error interface. This problem is not new and doesn't seem to have caused serious issues as far as we know.
Exactly it is rare case.
@beoran I did some initial analysis of the Go Corpus (https://github.com/rsc/corpus). I believe tryhard
in its current state could eliminate 41.7% of all err != nil
checks in the corpus. If I exclude the pattern "_test.go", this number rises to 51.1% (tryhard
only operates on functions which return errors, and it tends to not find many of those in tests). Caveat, take these numbers with a grain of salt, I got the denominator (i.e. the number of places in code we perform err != nil
checks) by using a hacked-up version of tryhard
, and ideally we'd wait until tryhard
reported these statistics itself.
Also, if tryhard
were to become type-aware, it could theoretically perform transformations like this:
// Before.
a, err := foo()
if err != nil {
return 0, nil, errors.Wrapf(err, "some message %v", b)
}
// After.
a, err := foo()
try(errors.Wrapf(err, "some message %v", b))
This takes advantage of errors.Wrap's behavior of returning nil
when the passed in error argument is nil
. (github.com/pkg/errors is also not unique in this respect, the internal library I use for doing error wrapping also preserves nil
errors, and would also work with this pattern, as would most error-handling libraries post-try
, I imagine). The new generation of support-libraries would probably also name these propagation helpers slightly differently.
Given that this would apply to 50% of non-test err != nil
checks out of the box, before any library evolution to support the pattern, it doesn't seem like the Go compiler and runtime are unique, as you suggest.
About the example with CreateDockerVolume
https://github.com/golang/go/issues/32437#issuecomment-508199875
I found exactly the same kind of usage. In lib i wrap error with context at each error, in usage of the lib i will like to use try
and add context in defer
for the whole function.
I tried to mimik this by adding an error handler function at the start, it's working fine:
func MyLib() error {
return errors.New("Error from my lib")
}
func MyOtherLib() error {
return errors.New("Error from my otherLib")
}
func Caller(a, b int) error {
eh := func(err error) error {
return fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, err)
}
err := MyLib()
if err != nil {
return eh(err)
}
err = MyOtherLib()
if err != nil {
return eh(err)
}
return nil
}
That will look fine and idiomatic with try+defer
func Caller(a, b int) (err error) {
defer fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, &err)
try(MyLib())
try(MyOtherLib())
return nil
}
@griesemer
The design doc currently has the following statements:
If the enclosing function declares other named result parameters, those result parameters keep whatever value they have. If the function declares other unnamed result parameters, they assume their corresponding zero values (which is the same as keeping the value they already have).
This implies that this program would print 1, instead of 0: https://play.golang.org/p/KenN56iNVg7.
As was pointed out to me on Twitter, this makes try
behave like a naked return, where the values being returned are implicit; to figure out what actual values are being returned, one might need to look at code at a significant distance from the call to try
itself.
Given that this property of naked returns (non-locality) is generally disliked, what are your thoughts on having try
always return the zero values of the non-error arguments (if it returns at all)?
Some considerations:
This might make some patterns involving the use of named return values unable to use try
. For instance, for implementations of io.Writer
, which need to return a count of bytes written, even in the partial write situation. That said, it seems like try
is error-prone in this case anyways (e.g. n += try(wrappedWriter.Write(...))
does not set n
to the right number in the event of an error return). It seems fine to me that try
will be rendered unusable for these kinds of use-cases, as scenarios where we need both values and an error are rather rare, in my experience.
If there is a function with many uses of try
, this might lead to code bloat, where there are many places in a function which need to zero-out the output variables. First, the compiler's pretty good at optimizing out unnecessary writes these days. And second, if it proves necessary, it seems like a straightforward optimization to have all try
-generated blocks goto
to a common shared function-wide label, which zeroes the non-error output values.
Also, as I'm sure you're aware, tryhard
is already implemented this way, so as a side benefit this will retroactively make tryhard
more correct.
@jonbodner https://go-review.googlesource.com/c/go/+/182717 gives you a pretty good idea of what that might look like.
Thanks for doing this exercise. However, this confirms to me what I suspected, the go source code itself has a lot of places where
try()
would be useful because the error is just passed on. However, as I can see from the experiments withtryhard
which others and myself submitted above, for many other codebasestry()
would not be very useful because in application code errors tend to be actually handled, not just passed on.
I would interpret this differently.
We have not had generics, so it will be hard to find code in the wild that would directly benefit from generics based on code written. That doesn't mean that generics would not be useful.
For me, there are 2 patterns I have used in code for error handling
These patterns are not widespread but they work. 1) is used in the standard library in its unexported functions and 2) is used extensively in my codebase for the last few years because I thought it was a nice way of using the orthogonal features to do simplified error decoration, and the proposal recommends and has blessed the approach. The fact that they are not widespread doesn't mean that they are not good. But as with everything, guidelines from the Go team recommending it will lead to them being used more in practice, in the future.
One final point of note is that decorating errors in every line of your code can be a bit too much. There will be some places where it makes sense to decorate errors, and some places where it doesn't. Because we didn't have great guidelines before, folks decided that it made sense to always decorate errors. But it may not add much value to always decorate each time a file didn't open, as it may be sufficient within the package to just have an error as "unable to open file: conf.json", as opposed to: "unable to get user name: unable to get db connection: unable to load system file: unable to open file: conf.json".
With the combination of the error values and the concise error handling, we are now getting better guidelines on how to handle errors. The preference seems to be:
I tend to feel like we keep overlooking the goals of the try proposal, and the high level things it tries to solve:
Many folks still have 1). Many folks have worked around 1) because better guidelines didn't exist before. But that doesn't mean that, after they start using it, their negative reaction would not change to become more positive.
Many folks can use 2). There may be disagreement on how much, but I gave an example where it makes my code much easier.
var u user = try(db.LoadUser(try(strconv.ParseInt(stringId)))
In java where exceptions are the norm, we would have:
User u = db.LoadUser(Integer.parseInt(stringId)))
Nobody would look at this code and say that we have to do it in 2 lines ie.
int id = Integer.parseInt(stringId)
User u = db.LoadUser(id))
We shouldn't have to do that here, under the guideline that try MUST not be called inline and MUST always be on its own line.
Furthermore, today, most code will do things like:
var u user
var err error
var id int
id, err = strconv.ParseInt(stringId)
if err != nil {
return u, errors.Wrap("cannot load userid from string: %s: %v", stringId, err)
}
u, err = db.LoadUser(id)
if err != nil {
return u, errors.Wrap("cannot load user given user id: %d: %v", id, err)
}
// now work with u
Now, someone reading this has to parse through these 10 lines, which in java would have been 1 line, and which could be 1 line with the proposal here. I visually have to mentally try to see what lines in here are really pertinent when I read this code. The boilerplate makes this code harder to read and grok.
I remember in my past life working on/with aspect oriented programming in java. There, the goal was to
This allows behaviors that are not central to the business logic (such as logging) to be added to a program without cluttering the code, core to the functionality. (quoting from wikipedia https://en.wikipedia.org/wiki/Aspect-oriented_programming ).
Error handling is not central to the business logic, but is central to correctness. The idea is the same - we shouldn't clutter our code with things not central to the business logic because "but error handling is very important". Yes it is, and yes we can put it to the side.
Regarding 4), Many proposals have suggested error handlers, which is code to the side that handles errors but doesn't clutter the business logic. The initial proposal has the handle keyword for it, and folks have suggested other things. This proposal says that we can leverage the defer mechanism for it, and just make that faster which was its achilles heel before. I know - I have made noise about the defer mechanism performance many times to the go team.
Note that tryhard
will not flag this code as something that can be simplified. But with try
and new guidelines, folks may want to simplify this code to a 1-liner and let the Error Frame capture the required context.
The context, which has been used very well in exception based languages, will capture that one tried to an error occured loading a user because the user id didn't exist, or because the stringId was not in a format that an integer id could be parsed from it.
Combine that with Error Formatter, and we can now richly inspect the error frame and the error itself and format the message nicely for users, without the hard to read a: b: c: d: e: underlying error
style that many folks have done and which we haven't had great guidelines for.
Remember that all these proposals together give us the solution we want: concise error handling without unnecessary boilerplate, while affording better diagnostics and better error formatting for users. These are orthogonal concepts but together become extremely powerful.
Finally, given 3) above, it is hard to use a keyword to solve this. By definition, a keyword doesn't allow extension to in the future pass a handler by name, or allow on-the-spot error decoration, or support goto semantics (instead of return semantics). With a keyword, we kinda have to have the full solution in mind first. And a keyword is not backwards compatible. The go team stated when Go 2 was starting, that they wanted to try to maintain backwards compatibility as much as possible. try
function maintains that, and if we see later that there's no extension necessary, a simple gofix can easily modify code to change try
function to a keyword.
My 2 cents again!
On 7/4/19, Sanjay Menakuru notifications@github.com wrote:
@griesemer
[ ... ]
As was pointed out to me on Twitter, this makestry
behave like a naked
return, where the values being returned are implicit; to figure out what
actual values are being returned, one might need to look at code at a
significant distance from the call totry
itself.Given that this property of naked returns (non-locality) is generally
disliked, what are your thoughts on havingtry
always return the zero
values of the non-error arguments (if it returns at all)?Naked returns are only permitted when return arguments are named. It
seems that try follows a different rule?
I like the overall idea of reusing defer
to address the problem. However, I'm wondering if try
keyword is the right way to do it. What if we could reuse already existing pattern. Something that everyone already knows from imports:
res, err := doSomething()
if err != nil {
return err
}
res, _ := doSomething()
Similar behaviour to what try
is going to do.
res, . := doSomething()
@piotrkowalczuk
This may be nicer syntax for it but I don't know how easy it would be to adapt Go to make this legal, both in Go and in syntax highlighters.
@balasanjay (and @lootch): Per your comment here, yes, the program https://play.golang.org/p/KenN56iNVg7 will print 1.
Since try
is only concerning itself with the error result, it leaves everything else alone. It could set other return values to their zero values, but it's not obviously clear why that would be better. For one it could cause more work when result values are named because they may have to be set to zero; yet the caller is (likely) going to ignore them if there was an error. But this is a design decision that could be changed if there are good reasons for it.
[edit: Note that this question (of whether to clear non-error results upon encountering an error) is not specific to the try
proposal. Any of the proposed alternatives that don't require an explicit return
will have to answer the same question.]
Regarding your example of a writer n += try(wrappedWriter.Write(...))
: Yes, in a situation where you need to increment n
even in case of an error, one cannot use try
- even if try
does not zero non-error result values. That is because try
only returns anything if there is no error: try
behaves cleanly like a function (but a function that may not return to the caller, but to the caller's caller). See the use of temporaries in the implementation of try
.
But in cases like your example one also would have to be careful with an if
statement and make sure to incorporate the returned byte count into n
.
But perhaps I am misunderstanding your concern.
@griesemer: I am suggesting that it is better to set the other return values to their zero values, because then it is clear what try
will do from just inspecting the callsite. It will either a) do nothing, or b) return from the function with zero values and the argument to try.
As specified, try
will retain the values of the non-error named return values, and one would therefore need to inspect the entire function to be clear on what values try
is returning.
This is the same issue with a naked return (having to scan the whole function to see what value is being returned), and was presumably the reason for filing https://github.com/golang/go/issues/21291. This, to me, implies that try
in a large function with named return values, would have to be discouraged under the same basis as naked returns (https://github.com/golang/go/wiki/CodeReviewComments#named-result-parameters). Instead, I suggest that try
be specified to always return the zero values of the non-error argument.
baffled and feel bad for the go team lately. try
is a clean and understandable solution to the specific problem it is trying to solve: verbosity in error handling.
the proposal reads: after a year long discussion we are adding this built-in. use it if you want less verbose code, otherwise continue doing what you do. the reaction is some not fully justified resistance for a opt-in feature for which team members have shown clear advantages!
i would further encourage the go team to make try
a variadic built-in if that's easy to do
try(outf.Seek(linkstart, 0))
try(io.Copy(outf, exef))
becomes
try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))
the next verbose thing could be those successive calls to try
.
I agree with nvictor for the most part, except for the variadic parameters for try
. I still believe it should have a spot for a handler and the variadic proposal can be pushing the readability boundary for myself.
@nvictor Go is a language that doesn't like non-orthogonal features. That means that if we, in the future, figure out a better error handling solution that isn't try
, it will be much more complicated to switch (if it doesn't get flat-out rejected because our current solution is "good enough").
I think there's a better solution out there than try
, and I'd rather take it slow and find that solution than settle for this one.
However I wouldn't be angry if this were added. It's not a bad solution, I just think we may be able to figure out a better one.
In my see, I want to try a block code, now try
like a handle err func
When reading this discussion (and discussions on Reddit), I didn't always feel like everyone was on the same page.
Thus, I wrote a little blog post that demonstrates how try
can be used: https://faiface.github.io/post/how-to-use-try/.
I tried to show multiple aspects of this proposal so that everybody can see what it can do and form a more informed (even if negative) opinion.
If I missed something important, please let me know!
@faiface I'm pretty sure you can replace
if err != nil {
return resps, err
}
with try(err)
.
Other than that - great article!
@DmitriyMV True! But I guess I'll keep it the way it is, so that there's at least one example of the classic if err != nil
, albeit not a very good one.
I have two concerns:
- named returns have been very confusing, and this encourages them with a new and important use case
- this will discourage adding context to errors
In my experience, adding context to errors immediately after each call site is critical to having code that can be easily debugged. And named returns have caused confusion for nearly every Go developer I know at some point.
A more minor, stylistic concern is that it's unfortunate how many lines of code will now be wrapped in
try(actualThing())
. I can imagine seeing most lines in a codebase wrapped intry()
. That feels unfortunate.I think these concerns would be addressed with a tweak:
a, b, err := myFunc() check(err, "calling myFunc on %v and %v", a, b)
check()
would behave much liketry()
, but would drop the behavior of passing through function return values generically, and instead would provide the ability to add context. It would still trigger a return.This would retain many of the advantages of
try()
:
- it's a built-in
- it follows the existing control flow WRT to defer
- it aligns with existing practice of adding context to errors well
- it aligns with current proposals and libraries for error wrapping, such as
errors.Wrap(err, "context message")
- it results in a clean call site: there's no boilerplate on the
a, b, err := myFunc()
line- describing errors with
defer fmt.HandleError(&err, "msg")
is still possible, but doesn't need to be encouraged.- the signature of
check
is slightly simpler, because it doesn't need to return an arbitrary number of arguments from the function it is wrapping.
This is good, I think go team really should take this one. This is better than try, more clearly !!!
@buchanae I'd be interested in what you think about my blog post because you argued that try
will discourage adding context to errors, while I'd argue that at least in my article it's even easier than usual.
I'm just gonna throw this out there at current stage. I will think about it some more, but I thought I post here to see what you think. Maybe I should open a new issue for this? I also posted this on #32811
So, what about doing some kind of generic C macro kind of thing instead to open up for more flexibility?
Like this:
define returnIf(err error, desc string, args ...interface{}) {
if (err != nil) {
return fmt.Errorf("%s: %s: %+v", desc, err, args)
}
}
func CopyFile(src, dst string) error {
r, err := os.Open(src)
:returnIf(err, "Error opening src", src)
defer r.Close()
w, err := os.Create(dst)
:returnIf(err, "Error Creating dst", dst)
defer w.Close()
...
}
Essentially returnIf will be replaced/inlined by that defined above. The flexibility there is that it's up to you what it does. Debugging this might be abit odd, unless the editor replaces it in the editor in some nice way. This also make it less magical, as you can clearly read the define. And also, this enables you to have one line that could potentially return on error. And able to have different error messages depending on where it happened (context).
Edit: Also added colon in front of the macro to suggest that maybe that can be done to clarify it's a macro and not a function call.
@nvictor
i would further encourage the go team to make
try
a variadic built-in
What try(foo(), bar())
would return if foo
and bar
don't return the same thing?
I'm just gonna throw this out there at current stage. I will think about it some more, but I thought I post here to see what you think. Maybe I should open a new issue for this? I also posted this on #32811
So, what about doing some kind of generic C macro kind of thing instead to open up for more flexibility?
@Chillance , IMHO, I think that a hygienic macro system like Rust (and many other languages) would give people a chance to play with ideas like try
or generics and then after experience is gained, the best ideas can become part of the language and libraries. But I also think that there's very little chance that such a thing will be added to Go.
@jonbodner there is currently a proposal to add hygienic macros in Go. No proposed syntax or anything yet, however there hasn't been much _against_ the idea of adding hygienic macros. #32620
@Allenyn, regarding @buchanae's earlier suggestion that you just quoted:
a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)
From what I have seen of the discussion, my guess is it would be an unlikely outcome here for the semantics of fmt
to get pulled into a builtin function. (See for example @josharian's response).
That said, it is not really needed, including because allowing a handler function can sidestep pulling fmt
semantics directly into a builtin. One such approach was proposed by @eihigh on the first day or so of discussion here, which is similar is spirit to @buchanae's suggestion, and which suggested tweaking the try
builtin to instead have the following signature:
func try(error, optional func(error) error)
Because this alternative try
does not return anything, that signature implies:
I don't want to trigger bikeshedding the name, but that form of try
might read better with an alternative name such as check
. One could imagine standard library helpers that could make the optional in-place annotation convenient, while defer
could remain an option for uniform annotation when desired.
There were some related proposals created later in #32811 (catch
as a builtin) and #32611 (on
keyword to allow on err, <statement>
). Those might be good places to discuss further, or to add a thumbs up or thumbs down, or to suggest possible tweaks to those proposals.
@jonbodner there is currently a proposal to add hygienic macros in Go. No proposed syntax or anything yet, however there hasn't been much _against_ the idea of adding hygienic macros. #32620
It's great that there's a proposal, but I suspect that the core Go team doesn't intend to add macros. However, I would be happy to be wrong about this as it would end all of the arguments about changes that currently require modifications to the language core. To quote a famous puppet, "Do. Or do not. There is no try."
@jonbodner I don't think that adding hygienic macros would end the argument. Quite the opposite. A common criticism is that try
"hides" the return. Macros would be strictly worse from this point of view, because anything would be possible in a macro. And even if Go would allow user-defined hygienic macros, we'd still have to debate if try
should be a built-in macro predeclared in the universe block, or not. It would be logical for those opposed to try
to be even more opposed to hygienic macros ;-)
@ngrilly there are several ways to make sure that macros stick out and are easy to see. The way Rust does it is that macros are always proceeded by !
(ie try!(...)
and println!(...)
).
I'd argue that if hygienic macros were adopted and easy to see, and didn't look like normal function calls, they would fit much better. We should opt for more general-purpose solutions rather than fix individual problems.
@thepudds I agree that adding an optional parameter of type func(error) error
could be useful (this possibility is discussed in the proposal, with some issues that would need to be solved), but I don't see the point of try
returning nothing. The try
proposed by the Go team is a more general tool.
@deanveloper Yes, the !
at the end of macros in Rust is clever. It reminds of exported identifiers starting with an uppercase letter in Go :-)
I'd agree having hygienic macros in Go if and only if we can preserve compilation speed and solve complex issues regarding tooling (refactoring tools would need to expand the macros to understand the semantics of the code, but must generate code with the macros unexpanded). It's hard. In the meantime, maybe try
could be renamed try!
? ;-)
A lightweight idea: if the body of an if/for construct contains a single statement, no need for braces provided this statement is on the same line as if
or for
. Example:
fd, err := os.Open("foo")
if err != nil return err
Note that at present an error
type is just an ordinary interface type. The compiler doesn't treat it as anything special. try
changes that. If the compiler is allowed to treat error
as special, I'd prefer a /bin/sh
inspired ||
:
fd, err := os.Open("foo") || return err
The meaning of such code would be fairly obvious to most programmers, there is no hidden control flow and, as at present this code is illegal, no working code is harmed.
Though I can imagine some of you are recoiling in horror.
@bakul In if err != nil return err
, how do you know where the expression err != nil
ends and where the statement return err
starts? Your idea would be a major change to the language grammar, much larger than what is proposed with try
.
Your second idea looks like catch |err| return err
in Zig. Personally, I'm not "recoiling in horror" and I'd say why not? But one should note that Zig also has a try
keyword, which is a shortcut for catch |err| return err
, and almost equivalent to what the Go team proposes here as a built-in function. So maybe the try
is enough and we don't need the catch
keyword? ;-)
@ngrilly, Currently <expr> <statement>
is not valid so I don't think this change would make the grammar any more ambiguous but may be a bit more fragile.
This would generate exactly the same code as the try proposal but a) the return is explicit here b) there is no nesting possible as with try and c) this would be familiar syntax to shell users (who far far outnumber zig users). There is no catch
here.
I brought this up as an alternative but to be frank I am perfectly fine with whatever the the core go language designers decide.
I've uploaded a slightly improved version of tryhard
. It now reports more detailed information on the input files. For instance, running against tip of the Go repo it reports now:
$ tryhard $HOME/go/src
...
--- stats ---
55620 (100.0% of 55620) function declarations
14936 ( 26.9% of 55620) functions returning an error
116539 (100.0% of 116539) statements
27327 ( 23.4% of 116539) if statements
7636 ( 27.9% of 27327) if <err> != nil statements
119 ( 1.6% of 7636) <err> name is different from "err" (use -l flag to list file positions)
6037 ( 79.1% of 7636) return ..., <err> blocks in if <err> != nil statements
1599 ( 20.9% of 7636) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
17 ( 0.2% of 7636) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
5907 ( 77.4% of 7636) try candidates (use -l flag to list file positions)
There's more to be done, but this gives a clearer picture. Specifically, 28% of all if
statements appear to be for error checking; this confirms that there is a significant amount of repetitive code. Of those error checks, 77% would be amenable to try
.
$ tryhard .
--- stats ---
2930 (100.0% of 2930) function declarations
1408 ( 48.1% of 2930) functions returning an error
10497 (100.0% of 10497) statements
2265 ( 21.6% of 10497) if statements
1383 ( 61.1% of 2265) if
0 ( 0.0% of 1383)
to list file positions)
645 ( 46.6% of 1383) return ...,
statements
738 ( 53.4% of 1383) more complex error handler in if
statements; prevent use of try (use -l flag to list file positions)
1 ( 0.1% of 1383) non-empty else blocks in if
statements; prevent use of try (use -l flag to list file positions)
638 ( 46.1% of 1383) try candidates (use -l flag to list file
positions)
$ go mod vendor
$ tryhard vendor
--- stats ---
37757 (100.0% of 37757) function declarations
12557 ( 33.3% of 37757) functions returning an error
88919 (100.0% of 88919) statements
20143 ( 22.7% of 88919) if statements
6555 ( 32.5% of 20143) if
109 ( 1.7% of 6555)
to list file positions)
5545 ( 84.6% of 6555) return ...,
statements
1010 ( 15.4% of 6555) more complex error handler in if
statements; prevent use of try (use -l flag to list file positions)
12 ( 0.2% of 6555) non-empty else blocks in if
statements; prevent use of try (use -l flag to list file positions)
5427 ( 82.8% of 6555) try candidates (use -l flag to list file
positions)
So, that is why I added colon in the macro example, so it would stick out and not look like a function call. Doesn't have to be colon of course. It's just an example. Also, a macro doesn't hide anything. You just look at what the macro does, and there you go. Like if it was a function, but it will be inlined. It's like you did a search and replace with the code piece from the macro into your functions where the macro usage was done. Naturally, if people make macros of macros and start to complicate things, well, blame yourself for making the code more complicated. :)
@mirtchovski
$ tryhard .
--- stats ---
2930 (100.0% of 2930) function declarations
1408 ( 48.1% of 2930) functions returning an error
10497 (100.0% of 10497) statements
2265 ( 21.6% of 10497) if statements
1383 ( 61.1% of 2265) if <err> != nil statements
0 ( 0.0% of 1383) <err> name is different from "err" (use -l flag to list file positions)
645 ( 46.6% of 1383) return ..., <err> blocks in if <err> != nil statements
738 ( 53.4% of 1383) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
1 ( 0.1% of 1383) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
638 ( 46.1% of 1383) try candidates (use -l flag to list file
positions)
$ go mod vendor
$ tryhard vendor
--- stats ---
37757 (100.0% of 37757) function declarations
12557 ( 33.3% of 37757) functions returning an error
88919 (100.0% of 88919) statements
20143 ( 22.7% of 88919) if statements
6555 ( 32.5% of 20143) if <err> != nil statements
109 ( 1.7% of 6555) <err> name is different from "err" (use -l flag to list file positions)
5545 ( 84.6% of 6555) return ..., <err> blocks in if <err> != nil statements
1010 ( 15.4% of 6555) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
12 ( 0.2% of 6555) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
5427 ( 82.8% of 6555) try candidates (use -l flag to list file
positions)
$
@av86743,
sorry, didn't consider that "Email replies do not support Markdown"
Some people have commented that it's not fair to count vendored code in tryhard
results. For instance, in the std library vendored code includes the generated syscall
packages which contain a lot of error checking and which may distort the overall picture. The newest version of tryhard
now excludes file paths containing "vendor"
by default (this can also be controlled with the new -ignore
flag). Applied to the std library at tip:
tryhard $HOME/go/src
/Users/gri/go/src/cmd/go/testdata/src/badpkg/x.go:1:1: expected 'package', found pkg
/Users/gri/go/src/cmd/go/testdata/src/notest/hello.go:6:1: expected declaration, found Hello
/Users/gri/go/src/cmd/go/testdata/src/syntaxerror/x_test.go:3:11: expected identifier
--- stats ---
45424 (100.0% of 45424) func declarations
8346 ( 18.4% of 45424) func declarations returning an error
71401 (100.0% of 71401) statements
16666 ( 23.3% of 71401) if statements
4812 ( 28.9% of 16666) if <err> != nil statements
86 ( 1.8% of 4812) <err> name is different from "err" (-l flag lists details)
3463 ( 72.0% of 4812) return ..., <err> blocks in if <err> != nil statements
1349 ( 28.0% of 4812) complex error handler in if <err> != nil statements; cannot use try (-l flag lists details)
17 ( 0.4% of 4812) non-empty else blocks in if <err> != nil statements; cannot use try (-l flag lists details)
3345 ( 69.5% of 4812) try candidates (-l flag lists details)
Now 29% (28.9%) of all if
statements appear to be for error checking (so slightly more than before), and of those about 70% appear to be candidates for try
(a bit fewer than before).
Change https://golang.org/cl/185177 mentions this issue: src: apply tryhard -err="" -ignore="vendor" -r $GOROOT/src
@griesemer you counted "complex error handlers" but not "single-statement error handlers".
If most "complex" handlers are a single statement, then on err
#32611 would produce about as much boilerplate savings as try()
-- 2 lines vs 3 lines x 70%. And on err
adds the benefit of a consistent pattern for the vast majority of errors.
@nvictor
try
is a clean and understandable solution to the specific problem it is trying to solve:
verbosity in error handling.
Verbosity in error handling is not _a problem_, it is Go's strength.
the proposal reads: after a year long discussion we are adding this built-in. use it if you want less verbose code, otherwise continue doing what you do. the reaction is some not fully justified resistance for a opt-in feature for which team members have shown clear advantages!
Yours _opt-in_ at writing time is a _must_ for all readers, including future-you.
clear advantages
If muddying control flow can be named 'an advantage', then yes.
try
, for the sake of java and C++ expats' habits, introduces magic that needs to be understood by all Gophers. In meantime sparing a minority few lines to write in a few places (as tryhard
runs have shown).
I'd argue that my way simpler onErr macro would spare more lines writing, and for the majority:
x, err = fa()
onErr break
r, err := fb(x)
onErr return 0, nil, err
if r, err := fc(x); onErr && triesleft > 0 {
triesleft--
continue retry
}
_(note that I am in the 'leave if err!= nil
alone' camp and above counter proposal was published to show a simpler solution that can make more whiners happy.)_
Edit:
i would further encourage the go team to make
try
a variadic built-in if that's easy to do
try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))
~Short to write, long to read, prone to slips or misunderstandings, flaky and dangerous at maintenance stage.~
I was wrong. Actually the variadic try
would be much better than nests, as we might write it by lines:
try( outf.Seek(linkstart, 0),
io.Copy(outf, exef),
)
and have try(…)
return after the first err.
I don't think this implicit error handle(syntax sugar) like try is good, because you can not handle multiple errors intuitively especially when you need need execute multiple functions sequentially.
I would suggest something like Elixir's with statement: https://www.openmymind.net/Elixirs-With-Statement/
Something like this below in golang:
switch a, b, err1 := go_func_01(),
apple, banana, err2 := go_func_02(),
fans, dissman, err3 := go_func_03()
{
normal_func()
else
err1 -> handle_err1()
err2 -> handle_err2()
_ -> handle_other_errs()
}
Is this kind of violation of the "Go prefers less features" and "adding features to Go would not make it better but bigger"? I am not sure...
I just wanna say, personally I am perfectly satisfied with the old way
if err != nil {
return …, err
}
And definitely I do not want to read the code written by others using the try
... The reason can be two folds:
try
s can be nested, i.e., try( ... try( ... try ( ... ) ... ) ... )
, hard to readIf you think that writing code in the old fashion for passing errors is tedious, why not just copy and paste since they are always doing the same job?
Well, you might think that, we do not always want to do the same job, but then you will have to write your "handler" function. So perhaps you lose nothing if you still write in the old way.
Isn't the performance of defer an issue with this proposed solution? I've benchmarked functions with and without defer and there was significant performance impact. I just googled someone else who's done such a benchmark and found a 16x cost. I don't remember mine being that bad but 4x slower rings a bell. How can something that might double or worse the run time of lots of functions be considered a viable general solution?
@eric-hawthorne Defers performance is a separate issue. Try doesn't inherently require defer and doesn't remove the ability to handle errors without it.
@fabian-f But this proposal could encourage replacing code in which someone is decorating the errors separately for each error inline within the scope of the if err != nil block. That would be a significant performance difference.
@eric-hawthorne Quoting the design doc:
Q: Isn’t using defer for wrapping errors going to be slow?
A: Currently a defer statement is relatively expensive compared to ordinary control flow. However, we believe that it is possible to make common use cases of defer for error handling comparable in performance with the current “manual” approach. See also CL 171758 which is expected to improve the performance of defer by around 30%.
Here was an interesting talk from Rust linked on Reddit. Most relevant part starts at 47:55
I tried tryhard on my largest public repo, https://github.com/dpinela/mflg, and got the following:
--- stats ---
309 (100.0% of 309) func declarations
36 ( 11.7% of 309) func declarations returning an error
305 (100.0% of 305) statements
73 ( 23.9% of 305) if statements
29 ( 39.7% of 73) if <err> != nil statements
0 ( 0.0% of 29) <err> name is different from "err"
19 ( 65.5% of 29) return ..., <err> blocks in if <err> != nil statements
10 ( 34.5% of 29) complex error handler in if <err> != nil statements; cannot use try
0 ( 0.0% of 29) non-empty else blocks in if <err> != nil statements; cannot use try
15 ( 51.7% of 29) try candidates
Most of the code in that repo is managing internal editor state and doesn't do any I/O, and so has few error checks - thus the places where try can be used are relatively limited. I went ahead and manually rewrote the code to use try where possible; git diff --stat
returns the following:
application.go | 42 +++++++++++-------------------------------
internal/atomicwrite/write.go | 35 ++++++++++++++---------------------
internal/clipboard/clipboard.go | 17 +++--------------
internal/config/config.go | 15 +++++++--------
internal/termesc/term.go | 5 +----
render.go | 8 ++------
6 files changed, 38 insertions(+), 84 deletions(-)
(Full diff here.)
Of the 10 handlers that tryhard reports as "complex", 5 are false negatives in internal/atomicwrite/write.go; they were using pkg/errors.WithMessage to wrap the error. The wrapping was exactly the same for all of them, so I rewrote that function to use try and deferred handlers. I ended up with this diff (+14, -21 lines):
@@ -20,21 +20,20 @@ const (
// The file is created with mode 0644 if it doesn't already exist; if it does, its permissions will be
// preserved if possible.
// If some of the directories on the path don't already exist, they are created with mode 0755.
-func Write(filename string, contentWriter func(io.Writer) error) error {
+func Write(filename string, contentWriter func(io.Writer) error) (err error) {
+ defer func() { err = errors.WithMessage(err, errString(filename)) }()
+
dir := filepath.Dir(filename)
- if err := os.MkdirAll(dir, defaultDirPerms); err != nil {
- return errors.WithMessage(err, errString(filename))
- }
- tf, err := ioutil.TempFile(dir, "mflg-atomic-write")
- if err != nil {
- return errors.WithMessage(err, errString(filename))
- }
+ try(os.MkdirAll(dir, defaultDirPerms))
+ tf := try(ioutil.TempFile(dir, "mflg-atomic-write"))
name := tf.Name()
- if err = contentWriter(tf); err != nil {
- os.Remove(name)
- tf.Close()
- return errors.WithMessage(err, errString(filename))
- }
+ defer func() {
+ if err != nil {
+ tf.Close()
+ os.Remove(name)
+ }
+ }()
+ try(contentWriter(tf))
// Keep existing file's permissions, when possible. This may race with a chmod() on the file.
perms := defaultPerms
if info, err := os.Stat(filename); err == nil {
@@ -42,14 +41,8 @@ func Write(filename string, contentWriter func(io.Writer) error) error {
}
// It's better to save a file with the default TempFile permissions than not save at all, so if this fails we just carry on.
tf.Chmod(perms)
- if err = tf.Close(); err != nil {
- os.Remove(name)
- return errors.WithMessage(err, errString(filename))
- }
- if err = os.Rename(name, filename); err != nil {
- os.Remove(name)
- return errors.WithMessage(err, errString(filename))
- }
+ try(tf.Close())
+ try(os.Rename(name, filename))
return nil
}
Notice the first defer, which annotates the error - I was able to fit it comfortably into one line thanks to WithMessage returning nil for a nil error. It seems that this kind of wrapper works about as well with this approach as the ones suggested in the proposal.
Two of the other "complex" handlers were in implementations of ReadFrom and WriteTo:
var line string
line, err = br.ReadString('\n')
b.lines = append(b.lines, line)
if err != nil {
if err == io.EOF {
err = nil
}
return
}
func (b *Buffer) WriteTo(w io.Writer) (int64, error) {
var n int64
for _, line := range b.lines {
nw, err := w.Write([]byte(line))
n += int64(nw)
if err != nil {
return n, err
}
}
return n, nil
}
These really were not amenable to try, so I left them alone.
Two others were code like this, where I'm returning an entirely different error than the one I checked (not just wrapping it). I left them unchanged as well:
n, err := strconv.ParseInt(s[1:], 16, 32)
if err != nil {
return Color{}, errors.WithMessage(err, fmt.Sprintf("color: parse %q", s))
}
The last one was in a function to load a config file, which always returns a (nonzero) config even if there's an error. It only had this one error check, so it didn't benefit much if at all from try:
-func Load() (*Config, error) {
- c := Config{
+func Load() (c *Config, err error) {
+ defer func() { err = errors.WithMessage(err, "error loading config file") }()
+
+ c = &Config{
TabWidth: 4,
ScrollSpeed: 1,
Lang: make(map[string]LangConfig),
}
- f, err := basedir.Config.Open(filepath.Join("mflg", "config.toml"))
- if err != nil {
- return &c, errors.WithMessage(err, "error loading config file")
- }
+ f := try(basedir.Config.Open(filepath.Join("mflg", "config.toml")))
defer f.Close()
- _, err = toml.DecodeReader(f, &c)
+ _, err = toml.DecodeReader(f, c)
if c.TextStyle.Comment == (Style{}) {
c.TextStyle.Comment = Style{Foreground: &color.Color{R: 0, G: 200, B: 0}}
}
if c.TextStyle.String == (Style{}) {
c.TextStyle.String = Style{Foreground: &color.Color{R: 0, G: 0, B: 200}}
}
- return &c, errors.WithMessage(err, "error loading config file")
+ return c, err
}
In fact, relying on try's behaviour of keeping the values of return parameters - like a naked return - feels, in my opinion, a bit harder to follow; unless I added more error checks, I'd stick to if err != nil
in this particular case.
TL;DR: try is only useful in a fairly small percentage (by line count) of this code, but where it helps, it really helps.
(Noob here). Another idea for multiple arguments. How about:
package trytest
import "fmt"
func errorInner() (string, error) {
return "", fmt.Errorf("inner error")
}
func errorOuter() (string, error) {
tryreturn errorInner()
return "", nil
}
func errorOuterWithArg() (string, error) {
var toProcess string
tryreturn toProcess, _ = errorOuter()
return toProcess + "", nil
}
func errorOuterWithArgStretch() (bool, string, error) {
var toProcess string
tryreturn false, ( toProcess,_ = errorOuterWithArg() )
return true, toProcess + "", nil
}
i.e. tryreturn triggers return of all values if error in last
value, else execution continues.
The principles I agree with:
-
try
) should at least be a statement, and ideally have the word return in it. Again, I think control flow in Go should be explicit.Syntax I support:
-
reterr _x_
statement (syntactic sugar for if err != nil { return _x_ }
, explicitly named to indicate it will return)So the common cases could be one nice short, explicit line:
func foo() error {
a, err := bar()
reterr err
b, err := baz(a)
reterr fmt.Errorf("getting the baz of %v: %v", a, err)
return nil
}
Instead of the 3 lines they are now:
func foo() error {
a, err := bar()
if err != nil {
return err
}
b, err := baz()
if err != nil {
return fmt.Errorf("getting the baz of %v: %v", a, err)
}
return nil
}
Things I Disagree with:
reterr
(every person who writes Go would benefit from reterr).@Qhesz It's not much different with try:
func foo() error {
a, err := bar()
try(err)
b, err := baz(a)
try(wrap(err, "getting the baz of %v", a))
return nil
}
@reusee I appreciate that suggestion, I didn't realise it could be used like that. It does seem a bit grating to me though, I'm trying to put my finger on why.
I think that "try" is an odd word to use in that way. "try(action())" makes sense in english, whereas "try(value)" doesn't really. I'd be more ok with it if it were a different word.
Also try(wrap(...))
evaluates wrap(...)
first right? How much of that do you think gets optimised away by the compiler? (Compared to just running if err != nil
?)
Also also #32611 is a vaguely similar proposal, and the comments have some enlightening opinions from both the core Go team and community members, in particular around the differences between keywords and builtin functions.
@Qhesz I agree with you about the naming. Maybe check
is more appropriate since either "check(action())" or "check(err)" reads well.
@reusee Which is a bit ironic, since the original draft design used check
.
On 7/6/19, mirtchovski notifications@github.com wrote:
$ tryhard .
--- stats ---
2930 (100.0% of 2930) function declarations
1408 ( 48.1% of 2930) functions returning an error
[ ... ]
I can't help being mischievous here: is that "functions returning an
error as the last argument"?
Lucio.
Final thought on my question above, I'd still prefer the syntax try(err, wrap("getting the baz of %v: %v", a, err))
, with wrap() only executed if err isn't nil. Instead of try(wrap(err, "getting the baz of %v", a))
.
@Qhesz A possible implementation of wrap
could be:
func wrap(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}
If the compiler can inline wrap
, then there is no performance difference between wrap
and if err != nil
clause.
@reusee I think you meant if err == nil
;)
@Qhesz A possible implementation of
wrap
could be:func wrap(err error, format string, args ...interface{}) error { if err == nil { return nil } return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err) }
If the compiler can inline
wrap
, then there is no performance difference betweenwrap
andif err != nil
clause.
%w
is not valid go
verb
(I presume he meant %v...)
So while a keyword would be preferable to write, I understand that a builtin is the preferred way to implement it.
I think I'd be on board with this proposal if
check
instead of try
panic()
.check!(...)
like Rust does, but I don't have a strong opinion on the specific syntax)~ Changed my mindThen that would be great, I'd use it at every function call I make.
And minor apologies to the thread, I only now found the comments above that outline pretty much what I just said.
@deanveloper fixed, thanks.
@olekukonko @Qhesz %w is newly added in tip: https://tip.golang.org/pkg/fmt/#Errorf
I apologise for not having read everything in this topic, but I'd like to mention something I haven't seen.
I see two separate cases where Go1's error handling can be annoying: "good" code that is correct but a bit repetitive; and "bad" code that is wrong, but mostly works.
In the first case, there really should be some logic in the if-err block, and moving to a try style construct discourages this good practice by making it harder to add extra logic.
In the second case, bad code is often of the form:
..., _ := might_error()
or just
might_error()
Where this occurs it's normally because the author doesn't think it's important enough to spend any time on error handling, and is just hoping everything works. This case could be improved by something very close to zero-effort, like:
..., XXX := might_error()
where XXX is a symbol that means "anything here should stop execution somehow". This would make it clear that this is not production ready code - the author is aware of an error case, but has not invested the time to decide what to do.
Of course this doesn't preclude a returnif handle(err)
type solution.
I'm against try, on balance, with compliments to the contributors on the nicely minimal design. I'm not a heavy Go expert, but was an early adopter and have code in production here and there. I work in the Serverless group in AWS and it looks like we'll be releasing a Go-based service later this year the first check-in of which was substantially written by me. I'm a really old guy, my path to go led through C, Perl, Java, and Ruby. My issues have appeared before in the very useful debate summary but I still think they're worth reiterating.
if err != nil { return }
(I think?) I personally like named return values and, given the benefits of error decorators, I suspect the proportion of named err return values is going to monotonically increase; which weakens the benefits of try.Again, congratulations to the community on the nice clean proposal and constructive discussion.
I've spent a significant amount of time jumping into and reading unfamiliar libraries or pieces of code over the last few years. Despite the tedium, if err != nil
provides a very easy to read, albeit vertically verbose, idiom. The spirit of what try()
is trying to accomplish is noble, and I do think there is something to be done, but this feature feels misprioritized and that the proposal is seeing the light of day too early (i.e. it should come after xerr
and generics have had a chance to marinate in a stable release for 6-12mo).
Introducing try()
appears to be a noble and worthwhile proposal (e.g. 29% - ~40% of if
statements are for if err != nil
checking). On the surface, it appears as though the reducing boilerplate associated with error handling will improve developer experiences. The tradeoff from the introduction of try()
comes in the form of cognitive load from the semi-subtle special-cases. One of Go's biggest virtues is that it's simple and there is very little cognitive load required to get something done (compared to C++ where the language spec is large and nuanced). Reducing one quantitative metric (LoC of if err != nil
) in exchange for increasing the quantitative metric of mental complexity is a tough pill to swallow (i.e. the mental tax on the most precious resource we have, brain-power).
In particular the new special cases for the way try()
is handled with go
, defer
, and named return variables makes try()
magical enough to make the code less explicit such that all authors or readers of Go code will have to know these new special cases in order to properly read or write Go and such burden did not exist previously. I like that there are explicit special cases for these situations - especially versus introducing some form of undefined behavior, but the fact that they need to exist in the first place indicates this is incomplete at the moment. If the special cases were for anything but error handling, it could be possible acceptable, but if we're already talking about something that could impact up to 40% of all LoC, these special cases will need to be trained into the entire community and that raises the cost of the cognitive load of this proposal to a high-enough level to warrant concern.
There is another example in Go where special-case rules are already a slippery cognitive slope, namely pinned and unpinned variables. Needing to pin variables isn't hard to understand in practice, but it gets missed because there is an implicit behavior here and this causes a mismatch between the author, reader, and what happens with the compiled executable at runtime. Even with linters such as scopelint
many developers still don't seem to grasp this gotcha (or worse, they know it but miss it because this gotcha slips their mind). Some of the most unexpected and difficult to diagnose runtime bugs from functioning programs have come from this particular problem (e.g. N objects all get populated with the same value instead of iterating over a slice and getting the expected distinct values). The failure domain from try()
is different than pinned variables, but there will be an impact on how people write code as a result.
IMNSHO, the xerr
and generics proposals need time to bake in production for 6-12mo before attempting to conquer the boilerplate from if err != nil
. Generics will likely pave the way for more rich error handling and a new idiomatic way of error handling. Once idiomatic error handling with generics begins to emerge, then and only then, does it make sense to revisit a discussion around try()
or whatever.
I don't pretend to know how generics will impact error handling, but it seems certain to me that generics will be used to create rich types that will almost certainly be used in error handling. Once generics have permeated libraries and have been added to error handling there may be an obvious way to repurpose the try()
to improve developer experience with regards to error handling.
The points of concern that I have are:
try()
isn't complicated in isolation, but it's cognitive overhead where none used to exist beforehand.err != nil
into the assumed behavior of try()
, the language is preventing the use of err
as a way of communicating state up the stack.try()
feels like forced cleverness but not clever enough to satisfy the explicit and obvious test that most of the Go language enjoys. Like most things involving subjective criteria, this is a matter of personal taste and experience and hard to quantify.switch
/case
statements and error wrapping seems untouched by this proposal, and a missed opportunity, which leads me to believe this proposal is a brick shy of making an unknown-unknown a known-known (or at worst, a known-unknown).Lastly, the try()
proposal feels like a new break in the dam that was holding back a flood of language-specific nuance like what we escaped by leaving C++ behind.
TL;DR: isn't a #nevertry
response so much as it is, "not now, not yet, and let's consider this again in the future after xerr
and generics mature in the ecosystem."
The #32968 linked above is not exactly a full counter-proposal, but it builds on my disagreement with the dangerous ability to nest that the try
macro possess. Unlike #32946 this one is a serious proposal, one that I hope lacks serious flaws (its yours to see, assess and comment, of course). Excerpt:
check
macro is not a one-liner: it helps the most where many repetitiveIt is a built-in, it does not nest in a single line, it allows for way more flows than try
and has no expectations about the shape of a code within. It does not encourage naked returns.
// built-in 'check' macro signature:
func check(Condition bool) {}
check(err != nil) // explicit catch: label.
{
ucred, err := getUserCredentials(user)
remote, err := connectToApi(remoteUri)
err, session, usertoken := remote.Auth(user, ucred)
udata, err := session.getCalendar(usertoken)
catch: // sad path
ucred.Clear() // cleanup passwords
remote.Close() // do not leak sockets
return nil, 0, err // dress before leaving
}
// happy path
// implicit catch: label is above last statement
check(x < 4)
{
x, y = transformA(x, z)
y, z = transformB(x, y)
x, y = transformC(y, z)
break // if x was < 4 after any of above
}
I have read as much as I can to gain an understanding of this thread. I am in favor of leaving things exactly as they are.
My reasons:
Also, perhaps I misunderstand the proposal, but usually, the try
construct in other languages results in multiple lines of code that all may potentially generate an error, and so they require error types. Adding complexity and often some sort of upfront error architecture and design effort.
In those cases (and I have done this myself), multiple try blocks are added. which lengthens code, and overshadows implementation.
If the Go implementation of try
differs from that of other languages, then even more confusion will arise.
My suggestion is to leave error handling the way it is
I know a lot of people have weighed in, but I would like to add my critique of the specification as is.
The part of the spec that most troubles me are these two requests:
Therefore we suggest to disallow try as the called function in a go statement.
...
Therefore we suggest to disallow try as the called function in a defer statement as well.
This would be the first builtin function of which this is true (you can even defer
and go
a panic
) edit because the result didn't need to be discarded. Creating a new builtin function that requires the compiler to give special control flow consideration seems like a large ask and breaks the semantic coherence of go. Every other control flow token in go is not a function.
A counter-argument to my complaint is that being able to defer
and go
a panic
is probably an accident and not very useful. However my point is that the semantic coherence of functions in go is broken by this proposal not that it is important that defer
and go
always make sense to use. There are probably lots of non-builtin functions that would never make sense to use defer
or go
with, but there is no explicit reason, semantically, why they can't be. Why does this builtin get to exempt itself from the semantic contract of funtions in go?
I know @griesemer doesn't want aesthetic opinions about this proposal injected into the discussion, but I do think one reason folks are finding this proposal aesthetically revolting is that they can sense it doesn't quite add up as a function.
The proposal says:
We propose to add a new function-like built-in called try with signature (pseudo-code)
func try(expr) (T1, T2, … Tn)
Except this isn't a function (which the proposal basically admits). It is, effectively, a one-off macro built into the language spec (if it were to be accepted). There are a few of problems with this signature.
What does it mean for a function to accept a generic expression as an argument, not to mention a called expression. Every other time the word "expression" is used in the spec it means something like an uncalled function. How is it that a "called" function can be thought of as being an expression, when in every other context its return values are what is semantically active. I.E. we think of a called function as being its return values. The exceptions, tellingly, are go
and defer
, which are both raw tokens not builtin functions.
Also this proposal gets its own function signature incorrect, or at least it doesn't make sense, the actual signature is:
func try(R1, R2, ... Rn) ((R|T)1, (R|T)2, ... (R|T)(n-1), ?Rn)
// where T is the return params of the function that try is being called from
// where `R` is a return value from a function, `Rn` must be an error
// try will return the R values if Rn is nil and not return Tn at all
// if Rn is not nil then the T values will be returned as well as Rn at the end
try
is called with arguments:try(arg1, arg2,..., err)
I think the reason this isn't addressed, is because try
is trying to accept an expr
argument which actually represents n number of return arguments from a function plus something else, further illustrative of the fact that this proposal breaks the semantic coherence of what a function is.
My final complain against this proposal is that it further breaks the semantic meaning of builtin functions. I am not indifferent to the idea that builtin functions sometimes need to be exempt from the semantic rules of "normal" functions (like not being able to assign them to variables, etc), but this proposal creates a large set of exemptions from the "normal" rules that seem to govern functions inside golang.
This proposal effectively makes try
a new thing that go hasn't had, it's not quite a token and it's not quite a function, it's both, which seems like a poor precedent to set in terms of creating semantic coherence throughout the language.
If we are going to add a new control-flow thing-ey I argue that it makes more sense to make it a raw token like goto
, et al. I know we aren't supposed to hawk proposals in this discussion, but by way of brief example, I think something like this makes a lot more sense:
f, err := os.Open("/dev/stdout")
throw err
While this does add an extra line of code I think it addresses every issue I raised, and also eliminates the whole "alternate" function signatures deficiency with try
.
edit1: note about exceptions to the defer
and go
cases where builtin can't be used, because results will be disregarded, whereas with try
it can't even really be said that the function has results.
@nathanjsweet the proposal you seek is #32611 :-)
@nathanjsweet Some of what you say turns out not to be the case. The language does not permit using defer
or go
with the pre-declared functions append cap complex imag len make new real
. It also does not permit defer
or go
with the spec-defined functions unsafe.Alignof unsafe.Offsetof unsafe.Sizeof
.
Thanks @nathanjsweet for your extensive comment - @ianlancetaylor already pointed out that your arguments are technically incorrect. Let me expand a bit:
1) You mention that the part of the spec which disallows try
with go
and defer
troubles you the most because try
would be the first built-in where this is true. This is not correct. The compiler already does not permit e.g., defer append(a, 1)
. The same is true for other built-ins that produce a result which is then dropped on the floor. This very restriction would also apply to try
for that matter (except when try
doesn't return a result). (The reason why we have even mentioned these restrictions in the design doc is to be as thorough as possible - they are truly irrelevant in practice. Also, if you read the design doc precisely, it does not say that we cannot make try
work with go
or defer
- it simply suggests that we disallow it; mostly as a practical measure. It's a "large ask" - to use your words - to make try
work with go
and defer
even though it's practically useless.)
2) You suggest that some people find try
"aesthetically revolting" is because it is not technically a function, and then you concentrate on the special rules for the signature. Consider new
, make
, append
, unsafe.Offsetof
: they all have specialized rules that we cannot express with an ordinary Go function. Look at unsafe.Offsetof
which has exactly the kind of syntactic requirement for its argument (it must be a struct field!) that we require of the argument for try
(it must be a single value of type error
or a function call returning an error
as last result). We do not express those signatures formally in the spec, for none of these built-ins because they don't fit into the existing formalism - if they would, they wouldn't have to be built-ins. Instead we express their rules in prose. That is _why_ they are built-ins which _are_ the escape hatch in Go, by design, from day one. Note also that the design doc is very explicit about this.
3) The proposal also does address what happens when try
is called with arguments (more than one): It's not permitted. The design doc states explicitly that try
accepts an (one) incoming argument expression.
4) You are stating that "this proposal breaks the semantic meaning of builtin functions". Nowhere does Go restrict what a built-in can do and what it cannot do. We have complete freedom here.
Thanks.
@griesemer
Note also that the design doc is very explicit about this.
Can you point this out. I was surprised to read this.
You are stating that "this proposal breaks the semantic meaning of builtin functions". Nowhere does Go restrict what a built-in can do and what it cannot do. We have complete freedom here.
I think this is a fair point. However, I do think that there is what is spelled out in the design docs and what feels like "go" (which is something Rob Pike talks about a lot). I think it is fair for me to say that the try
proposal expands the ways in which builtin functions break the rules by which we expect functions to behave, and I did acknowledge that I understand why this is necessary for other builtins, but I think in this case the expansion of breaking the rules is:
panic
and os.Exit
do)unsafe.Offsetof
as a case where there is a syntactic requirement for a function call (it is surprising to me actually that this causes a compile-time error, but that's another issue), but the syntactic requirement, in this case, is a different syntactic requirement than the one you stated. unsafe.Offsetof
requires one argument, whereas try
requires an expression that would look, in every other context, like a value returned from a function (i.e. try(os.Open("/dev/stdout"))
) and could be safely assumed in every other context to return only one value (unless the expression looked like try(os.Open("/dev/stdout")...)
). @nathanjsweet wrote:
Note also that the design doc is very explicit about this.
Can you point this out. I was surprised to read this.
It's in the "Conclusions" section of the proposal:
In Go, built-ins are the language escape mechanism of choice for operations that are irregular in some way but which don’t justify special syntax.
I'm surprised you have missed it ;-)
@ngrilly I don't mean in this proposal, I mean in the go language specification. I got the impression that @griesemer was saying that the go language spec calls builtin functions out as being the specifically useful mechanism for breaking syntactic convention.
@nathanjsweet
Counter-intuitive in some ways. This is the first function that changes control flow logic in a way that doesn't unwind the stack (like panic and os.Exit do)
I don't think that os.Exit
unwinds the stack in any useful sense. It terminates the program immediately without running any deferred functions. It seems to me that os.Exit
is the odd one out here, as both panic
and try
run deferred functions and travel up the stack.
I agree that os.Exit
is the odd one out, but it has to be that way. os.Exit
stops all goroutines; it wouldn't make sense to only run the deferred functions of just the goroutine that calls os.Exit
. It should either run all deferred functions, or none. And it's much much easier to run none.
Executed tryhard
on our codebase and this is what we got:
--- stats ---
15298 (100.0% of 15298) func declarations
3026 ( 19.8% of 15298) func declarations returning an error
33941 (100.0% of 33941) statements
7765 ( 22.9% of 33941) if statements
3747 ( 48.3% of 7765) if <err> != nil statements
131 ( 3.5% of 3747) <err> name is different from "err"
1847 ( 49.3% of 3747) return ..., <err> blocks in if <err> != nil statements
1900 ( 50.7% of 3747) complex error handler in if <err> != nil statements; cannot use try
19 ( 0.5% of 3747) non-empty else blocks in if <err> != nil statements; cannot use try
1789 ( 47.7% of 3747) try candidates
First, I want to clarify that because Go (before 1.13) lacks context in errors, we implemented our own error type that implements the error
interface, some functions are declared as returning foo.Error
instead of error
, and it looks like this analyzer didn't catch that so these results aren't "fair".
I was in the camp of "yes! let's do this", and I think it will be an interesting experiment for 1.13 or 1.14 betas, but I'm concerned by the _"47.7% ... try candidates"_. It now means there are 2 ways of doing things, which I don't like. However there are also 2 ways of creating a pointer (new(Foo)
vs &Foo{}
) as well as 2 ways of creating a slice or map with make([]Foo)
and []Foo{}
.
Now I'm on the camp of "let's _try_ this" :^) and see what the community thinks. Perhaps we will change our coding patterns to be lazy and stop adding context, but maybe that's OK if errors get better context from the xerrors
impl that's coming anyways.
Thanks, @Goodwine for providing more concrete data!
(As an aside, I made a small change to tryhard
last night so it splits the "complex error handler" count into two counts: complex handlers, and returns of the form return ..., expr
where the last result value is not <err>
. This should provide some additional insight.)
What about amending the proposal to be variadic instead of this weird expression argument?
That would solve a lot of problems. In the case where people wanted to just return the error the only thing that would change is the explicit variadic ...
. E.G.:
try(os.Open("/dev/stdout")...)
however, folks who want a more flexible situation can do something like:
f, err := os.Open("/dev/stdout")
try(WrapErrorf(err, "whatever wrap does: %v"))
One thing that this idea does is make the word try
less appropriate, but it keeps backwards compatibility.
@nathanjsweet wrote:
I don't mean in this proposal, I mean in the go language specification.
Here are the extracts you were looking for in the language spec:
In the "Expression statements" section:
The following built-in functions are not permitted in statement context:
append cap complex imag len make new real unsafe.Alignof unsafe.Offsetof unsafe.Sizeof
In the "Go statements" and "Defer statements" sections:
Calls of built-in functions are restricted as for expression statements.
In the "Built-in functions" section:
The built-in functions do not have standard Go types, so they can only appear in call expressions; they cannot be used as function values.
@nathanjsweet wrote:
I got the impression that @griesemer was saying that the go language spec calls builtin functions out as being the specifically useful mechanism for breaking syntactic convention.
Built-in functions don't break Go syntactic conventions (parenthesis, commas between arguments, etc.). They use the same syntax as user-defined functions, but they permit things than cannot be done in user-defined functions.
@nathanjsweet That was already considered (in fact it was an oversight) but it makes try
not extensible. See https://go-review.googlesource.com/c/proposal/+/181878 .
More generally, I think you are are focussing your critique on the the wrong thing: The special rules for the try
argument are really a non-issue - virtually every built-in has special rules.
@griesemer thanks for working on this and taking the time to respond to community concerns. I'm sure you've responded to a lot of the same questions at this point. I realize that it's really hard to solve these problems and maintain backward compatibility at the same time. Thanks!
@nathanjsweet Regarding your comment here:
See the Conclusion section which prominently talks about the role of built-ins in Go.
Regarding your comments on try
extending the built-ins in different ways: Yes, the requirement that unsafe.Offsetof
puts on its argument is a different one than that of try
. But both expect syntactically an expression. Both have some additional restrictions on that expression. The requirement for try
fit so easily into Go's syntax that none of the front-end parsing tools need to be adjusted. I understand that it me feel unusual to you, but that is not the same as a technical reason against it.
@griesemer the latest _tryhard_ counts "complex error handlers" but not "single-statement error handlers". Could that be added?
@networkimprov What is a single-statement error handler? An if
block that contains a single non-return statement?
@griesemer, a single-statement error handler is an if err != nil
block that contains _any_ single statement, including a return.
@networkimprov Done. "complex handlers" are now split into "single statement then branch" and "complex then branch".
That said, note that these counts may be misleading: For instance, these counts include any if
statement that checks any variable against nil (if -err=""
which is now the default for tryhard
). I should fix this. In short, as is tryhard
overestimates the number of complex or single-statement handler opportunities by a lot. For an example, see archive/tar/common.go
, line 701.
@networkimprov tryhard
now provides more accurate counts on why an error check is not a try
candidate. The overall number of try
counts is unchanged, but the number of opportunities for more single and complex handlers is now more accurate (and roughly 50% smaller than it was before, because before any complex then
branch of an if
statement was considered as long as the if
contained an <varname> != nil
check, whether it was involving error checking or not).
If anyone wants to try out try
in a slightly more hands on way, I've created a WASM playground here with a prototype implementation:
https://ccbrown.github.io/wasm-go-playground/experimental/try-builtin/
And if anyone is actually interested in compiling code locally with try, I have a Go fork with what I believe is a fully functional / up-to-date implementation here: https://github.com/ccbrown/go/pull/1
i like 'try'. i find managing the local state of err, and using := vs = with err, along w/ associated imports, to be a regular distraction. also, i don't see this as creating two ways to do the same thing, more like two cases, one where you want to pass along an err without acting on it, the other where you explicitly want to handle it in the calling function eg. logging.
I ran tryhard
against a small internal project that I worked on over a year ago. The directory in question has the code for 3 servers ("microservices", I suppose), a crawler that runs periodically as a cron job, and a few command-line tools. It also has fairly comprehensive unit tests. (FWIW, the various pieces have been running smoothly for over a year, and it has proved straightforward to debug and resolve any issues that arise)
Here are the stats:
--- stats ---
370 (100.0% of 370) func declarations
115 ( 31.1% of 370) func declarations returning an error
1159 (100.0% of 1159) statements
258 ( 22.3% of 1159) if statements
123 ( 47.7% of 258) if <err> != nil statements
64 ( 52.0% of 123) try candidates
0 ( 0.0% of 123) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
54 ( 43.9% of 123) { return ... zero values ..., expr }
2 ( 1.6% of 123) single statement then branch
3 ( 2.4% of 123) complex then branch; cannot use try
1 ( 0.8% of 123) non-empty else branch; cannot use try
Some commentary:
1) 50% of all if
statements in this codebase were doing error-checking, and try
could replace ~half of those. This means a quarter of all if
statements in this (small) codebase are a typed-out version of try
.
2) I should note that this is surprisingly high to me, because a few weeks before starting on this project, I happened to read of a family of internal helper functions (status.Annotate
) that annotate an error message but preserve the gRPC status code. For instance, if you call an RPC and it returns an error with an associated status code of PERMISSION_DENIED, the returned error from this helper function would still have an associated status code of PERMISSION_DENIED (and theoretically, if that associated status code was propagated all the way up to an RPC handler, then the RPC would fail with that associated status code). I had resolved to use these functions for everything on this new project. But apparently, for 50% of all errors, I simply propagated an error unannotated. (Before running tryhard
, I had predicted 10%).
3) status.Annotate
happens to preserve nil
errors (i.e. status.Annotatef(err, "some message: %v", x)
will return nil
iff err == nil
). I looked through all of the non-try candidates of the first category, and it seems like all would be amenable to the following rewrite:
```
// Before
enc, err := keymaster.NewEncrypter(encKeyring)
if err != nil {
return status.Annotate(err, "failed to create encrypter")
}
// After
enc, err := keymaster.NewEncrypter(encKeyring)
try(status.Annotate(err, "failed to create encrypter"))
```
To be clear, I'm not saying this transformation is always necessarily a good idea, but it seemed worth mentioning since it boosts the count significantly to a bit under half of all `if` statements.
4) defer
-based error annotation seems somewhat orthogonal to try
, to be honest, since it will work with and without try
. But while looking through the code for this project, since I was looking closely at the error-handling, I happened to notice several instances where callee-generated errors would make more sense. As one example, I noticed several instances of code calling gRPC clients like this:
```
resp, err := s.someClient.SomeMethod(ctx, req)
if err != nil {
return ..., status.Annotate(err, "failed to call SomeMethod")
}
```
This is actually a bit redundant in retrospect, since gRPC already prefixes its errors with something like "/Service.Method to [ip]:port : ".
There was also code that called standard library functions using the same pattern:
```
hreq, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
return status.Annotate(err, "http.NewRequest failed")
}
```
In retrospect, this code demonstrates two issues: first, `http.NewRequest` isn't calling a gRPC API, so using `status.Annotate` was unnecessary, and second, assuming the standard library also return errors with callee context, this particular use of error annotation was unnecessary (although I am fairly certain the standard library does not consistently follow this pattern).
In any case, I thought it was an interesting exercise to come back to this project and look carefully at how it handled errors.
One thing, @griesemer: does tryhard
have the right denominator for "non-try candidates"? It looks like its using "try candidates" as the denominator, which doesn't really make sense.
Edit: answered below, I misread the stats.
EDIT: What was meant to be feedback devolved into a proposal, which we were explicitly asked not to do here. I moved my comment to a gist.
@balasanjay Thanks for your fact-based comment; that is very helpful.
Regarding your question about tryhard
: The "non-try candidates" (better title suggestion welcome) are simply the number of cases where the if
statement satisfied all criteria for an "error check" (i.e., we had what looked like an assignment to an error variable <err>
, followed by an if <err> != nil
check in the source), but where we can't easily use try
because of the code in the if
blocks. Specifically, in order of appearance in the "non-try candidates" output, these are if
statements which have a return
statement that return something else than <err>
at the end, if
statements with a single more complex return
(or other) statement, if
statements with multiple statements in the "then" branch, and if
statements with non-empty else
branch. Some of these if
statements may have multiple of these conditions satisfied simultaneously, so these numbers don't just add up. They are intended to give an idea of what went wrong for try
to be usable.
I've made the most recent adjustments to this today (you ran the latest version). Before the last change, some of these conditions where counted even if there was no error check involved, which seemed to make less sense because it looked like try
couldn't be used in many more cases when in fact try
didn't make sense in those cases in the first place.
Most importantly though, for a given codebase, the overall number of try
candidates has not changed with these refinements, since the relevant conditions for try
remained the same.
If you have a better suggestion how and/or what to measure, I'd be happy to hear that. I've made several adjustments based on community feedback. Thanks.
@subfuzion Thanks for your comment, but we are are not looking for alternative proposals. Please see https://github.com/golang/go/issues/32437#issuecomment-501878888 . Thanks.
In the interest of being counted, regardless of the outcome:
I'm of the view, along with my team, that while the try
framework as proposed by Rob is a reasonable and interesting idea it doesn't reach the level where it would be appropriate as a builtin. A standard library package would be a much more appropriate approach until usage patterns are established in practice. If try
came into the language that way we'd use it in a number of different places.
On a more general note, Go's combination of a very stable core language and a very rich standard library is worth preserving. The slower the language team moves on core language changes the better. The x -> stdlib
pipeline remains a strong approach for this sort of thing.
@griesemer Ah, sorry. I misread the stats, it's using the "if err != nil statements" counter (123) as the denominator, not the "try candidates" counter (64) as the denominator. I'll strike that question.
Thanks!
@mattpalmer Usage patterns have established themselves for about a decade. It's these exact usage patterns that directly influenced the design of try
. What usage patterns are you referring to?
@griesemer Sorry, that's my fault -- what started off in my mind as explaining what bothered me about try
devolved into its own proposal to make my point about not adding it. That was clearly against stated ground rules (not to mention that unlike this proposal for a new built-in function, it introduces a new operator). Would it be helpful to delete the comment to keep the conversation streamlined (or is that considered poor form)?
@subfuzion I wouldn't worry about it. It's a controversial suggestion and there's lots of proposals. Many are outlandish
We have iterated on that design multiple times and solicited feedback from many people before we felt comfortable enough to post it and recommending advancing it to the actual experiment phase, but we haven't done the experiment yet. It does make sense to go back to the drawing board if the experiment fails, or if feedback tells us in advance that it will clearly fail.
@griesemer can you elaborate on the specific metrics the team will be using to establish the success or failure of the experiment?
@iand
I asked this of @rsc a while ago (https://github.com/golang/go/issues/32437#issuecomment-503245958):
@rsc
There will be no shortage of locations where this convenience can be placed. What metric is being sought that will prove the substance of the mechanism aside from that? Is there a list of classified error handling cases? How will value be derived from the data when much of the public process is driven by sentiment?
The answer was purposed, but uninspiring and lacking substance (https://github.com/golang/go/issues/32437#issuecomment-503295558):
The decision is based on how well this works in real programs. If people show us that try is ineffective in the bulk of their code, that's important data. The process is driven by that kind of data. It is not driven by sentiment.
Additional sentiment was offered (https://github.com/golang/go/issues/32437#issuecomment-503408184):
I was surprised to find a case in which
try
led to clearly better code, in a way that had not been discussed before.
Eventually, I answered my own question "Is there a list of classified error handling cases?". There will effectively be 6 modes of error handling - Manual Direct, Manual Pass-through, Manual Indirect, Automatic Direct, Automatic Pass-through, Automatic Indirect. Currently, it is only common to use 2 of those modes. The indirect modes, that have a significant amount of effort being put into their facilitation, appear strongly prohibitive to most veteran Gophers and that concern is seemingly being ignored. (https://github.com/golang/go/issues/32437#issuecomment-507332843).
Further, I suggested that automated transforms be vetted prior to transformation to try to ensure the value of the results (https://github.com/golang/go/issues/32437#issuecomment-507497656). Over time, thankfully, more of the results being offered do seem to have better retrospectives, but this still does not address the impact of the indirect methods in a sober and concerted manner. After all (in my opinion), just as users should be treated as hostile, devs should be treated as lazy.
The failing of the current approach to miss valuable candidates was also pointed out (https://github.com/golang/go/issues/32437#issuecomment-507505243).
I think it's worth being noisy about this process being generally lacking and notably tone-deaf.
@iand The answer given by @rsc is still valid. I'm not sure which part of that answer is "lacking substance" or what it takes to be "inspiring". But let me try to add more "substance":
The purpose of the proposal evaluation process is to ultimately identify "whether a change has delivered the expected benefits or created any unexpected costs" (step 5 in the process).
We have passed step 1: The Go team has selected specific proposals that seem worth accepting; this proposal is one of them. We would not have selected it if we had not thought about it pretty hard and deemed it worthwhile. Specifically, we do believe that there is a significant amount of boilerplate in Go code related solely to error handling. The proposal is also not coming out of thin air - we've been discussing this for over a year in various forms.
We are currently at step 2, thus still quite a bit away from a final decision. Step 2 is for gathering feedback and concerns - which there seem to be plenty of. But to be clear here: So far there was only a single comment pointing out a _technical_ deficiency with the design, which we corrected. There were also quite a few comments with concrete data based on real code which indicated that try
would indeed reduce boilerplate and simplify code; and there were a few comments - also based on data on real code - that showed that try
would not help much. Such concrete feedback, based on actual data, or pointing out technical deficiencies, is actionable and very helpful. We will absolutely take this into account.
And then there was the vast amount of comments that is essentially personal sentiment. This is less actionable. This is not to say that we are ignoring it. But just because we are sticking to the process does not mean we are "tone-deaf".
Regarding these comments: There are perhaps two, maybe three dozen vocal opponents of this proposal - you know who you are. They are dominating this discussion with frequent posts, sometimes multiple a day. There is little new information to be gained from this. The increased number of posts also does not reflect a "stronger" sentiment by the community; it just means that these people are more vocal than others.
@iand The answer given by @rsc is still valid. I'm not sure which part of that answer is "lacking substance" or what it takes to be "inspiring". But let me try to add more "substance":
@griesemer I'm sure it was unintentional but I'd like to note that none of the words you've quoted were mine but a later commenter's.
That aside, I hope that in addition to reducing boilerplate and simplifying the success of try
will be judged on whether it allows us to write better and clearer code.
@iand Indeed - that was just an oversight of mine. My apologies.
We do believe that try
does allow us to write more readable code - and much of the evidence we have received from real code and our own experiments with tryhard
show significant cleanups. But readability is more subjective and harder to quantify.
@griesemer
What usage patterns are you referring to?
I'm referring to the usage patterns that will develop around try
over time, not the existing nil-check pattern for handling errors. The potential for misuse and abuse is a big unknown, especially with the ongoing influx of programmers who have used semantically different versions of try-catch in other languages.
All this and the considerations about the long term stability of the core language lead me to think that introducing this feature at the level of the x packages or the standard library (either as package errors/try
or as errors.Try()
) would be preferable to introducing it as a builtin.
@mattparlmer Correct me if I'm wrong, but I believe this proposal would have to be in the Go runtime in order to use g's, m's (necessary to override execution flow).
@fabian-f
@mattparlmer Correct me if I'm wrong, but I believe this proposal would have to be in the Go runtime in order to use g's, m's (necessary to override execution flow).
That's not the case; as the design doc notes, it is implementable as a compile-time syntax tree transformation.
That is possible because the semantics of try
can be fully expressed in terms of if
and return
; it doesn't really "override execution flow" any more than if
and return
do.
Here's a tryhard
report from my company's 300k-line Go codebase:
Initial run:
--- stats ---
13879 (100.0% of 13879) func declarations
4381 ( 31.6% of 13879) func declarations returning an error
38435 (100.0% of 38435) statements
8028 ( 20.9% of 38435) if statements
4496 ( 56.0% of 8028) if <err> != nil statements
453 ( 10.1% of 4496) try candidates
4 ( 0.1% of 4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
3066 ( 68.2% of 4496) { return ... zero values ..., expr }
356 ( 7.9% of 4496) single statement then branch
345 ( 7.7% of 4496) complex then branch; cannot use try
63 ( 1.4% of 4496) non-empty else branch; cannot use try
We have a convention of using juju's errgo package (https://godoc.org/github.com/juju/errgo) to mask errors and add stack trace information to them, which would prevent most rewrites from happening. That does mean that we are unlikely to adopt try
, for the same reason that we generally eschew naked error returns.
Since it seems like it might be a helpful metric, I removed errgo.Mask()
calls (which return the error without annotation) and re-ran tryhard
. This is an estimate of how many error checks could be rewritten if we didn't use errgo:
--- stats ---
13879 (100.0% of 13879) func declarations
4381 ( 31.6% of 13879) func declarations returning an error
38435 (100.0% of 38435) statements
8028 ( 20.9% of 38435) if statements
4496 ( 56.0% of 8028) if <err> != nil statements
3114 ( 69.3% of 4496) try candidates
7 ( 0.2% of 4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
381 ( 8.5% of 4496) { return ... zero values ..., expr }
358 ( 8.0% of 4496) single statement then branch
345 ( 7.7% of 4496) complex then branch; cannot use try
63 ( 1.4% of 4496) non-empty else branch; cannot use try
So, I guess ~70% of error returns would otherwise be compatible with try
.
Lastly, my primary concern with the proposal does not seem to be captured in any of the comments I read nor the discussion summaries:
This proposal significantly increases the relative cost of annotating errors.
Presently, the marginal cost of adding some context to an error is very low; it's barely more than typing the format string. If this proposal were adopted, I worry that engineers would increasingly prefer the aesthetic offered by try
, both because it makes their code "look more sleek" (which I'm sad to say is a consideration for some folks, in my experience), and now requires an additional block to add context. They could justify it based on a "readability" argument, how adding context expands the method by another 3 lines and distracts the reader from the main point. I think that corporate code bases are unlike the Go standard library in the sense that making it easy to do the right thing likely has a measurable impact on the resulting code quality, code reviews are of varying quality, and team practices vary independently of each other. Anyway, as you said before, we could always not adopt try
for our codebase.
Thanks for the consideration
@mattparlmer
All this and the considerations about the long term stability of the core language lead me to think that introducing this feature at the level of the x packages or the standard library (either as package
errors/try
or aserrors.Try()
) would be preferable to introducing it as a builtin.
try
cannot be implemented as a library function; there is no way for a function to return from its caller (enabling that has been proposed as #32473) and, like most other built-ins, there is also no way to express the signature of try
in Go. Even with generics, that is unlikely to become possible; see the design doc FAQ, near the end.
Also, implementing try
as a library function would require it to have a more verbose name, which partly defeats the point of using it.
However, it can be - and has been twice - implemented as a source code preprocessor: see https://github.com/rhysd/trygo and https://github.com/lunixbochs/og.
It looks like ~60% of code base from tegola would be able to make use of this feature.
Here is tryhard's output for the tegola project: (http://github.com/go-spatial/tegola)
--- try candidates ---
1 tegola/atlas/atlas.go:84
2 tegola/atlas/map.go:232
3 tegola/atlas/map.go:238
4 tegola/atlas/map.go:248
5 tegola/atlas/map.go:253
6 tegola/basic/geometry_math.go:248
7 tegola/basic/geometry_math.go:251
8 tegola/basic/geometry_math.go:268
9 tegola/basic/geometry_math.go:276
10 tegola/basic/json_marshal.go:33
11 tegola/basic/json_marshal.go:153
12 tegola/basic/json_marshal.go:276
13 tegola/cache/azblob/azblob.go:54
14 tegola/cache/azblob/azblob.go:61
15 tegola/cache/azblob/azblob.go:67
16 tegola/cache/azblob/azblob.go:74
17 tegola/cache/azblob/azblob.go:80
18 tegola/cache/azblob/azblob.go:105
19 tegola/cache/azblob/azblob.go:109
20 tegola/cache/azblob/azblob.go:204
21 tegola/cache/azblob/azblob.go:259
22 tegola/cache/file/file.go:42
23 tegola/cache/file/file.go:56
24 tegola/cache/file/file.go:110
25 tegola/cache/file/file.go:116
26 tegola/cache/file/file.go:129
27 tegola/cache/redis/redis.go:41
28 tegola/cache/redis/redis.go:46
29 tegola/cache/redis/redis.go:51
30 tegola/cache/redis/redis.go:56
31 tegola/cache/redis/redis.go:70
32 tegola/cache/redis/redis.go:79
33 tegola/cache/redis/redis.go:84
34 tegola/cache/s3/s3.go:85
35 tegola/cache/s3/s3.go:102
36 tegola/cache/s3/s3.go:112
37 tegola/cache/s3/s3.go:118
38 tegola/cache/s3/s3.go:123
39 tegola/cache/s3/s3.go:138
40 tegola/cache/s3/s3.go:164
41 tegola/cache/s3/s3.go:172
42 tegola/cache/s3/s3.go:179
43 tegola/cache/s3/s3.go:284
44 tegola/cache/s3/s3.go:340
45 tegola/cmd/tegola/cmd/cache/format.go:97
46 tegola/cmd/tegola/cmd/cache/seed_purge.go:94
47 tegola/cmd/tegola/cmd/cache/seed_purge.go:103
48 tegola/cmd/tegola/cmd/cache/seed_purge.go:170
49 tegola/cmd/tegola/cmd/cache/tile_list.go:51
50 tegola/cmd/tegola/cmd/cache/tile_list.go:64
51 tegola/cmd/tegola/cmd/cache/tile_name.go:35
52 tegola/cmd/tegola/cmd/cache/tile_name.go:43
53 tegola/cmd/tegola/cmd/root.go:58
54 tegola/cmd/tegola/cmd/root.go:61
55 tegola/cmd/xyz2svg/cmd/draw.go:62
56 tegola/cmd/xyz2svg/cmd/draw.go:70
57 tegola/cmd/xyz2svg/cmd/draw.go:214
58 tegola/config/config.go:96
59 tegola/internal/env/parse.go:30
60 tegola/internal/env/parse.go:69
61 tegola/internal/env/parse.go:116
62 tegola/internal/env/parse.go:174
63 tegola/internal/env/parse.go:221
64 tegola/internal/env/types.go:67
65 tegola/internal/env/types.go:86
66 tegola/internal/env/types.go:105
67 tegola/internal/env/types.go:124
68 tegola/internal/env/types.go:143
69 tegola/maths/makevalid/main.go:189
70 tegola/maths/makevalid/main.go:207
71 tegola/maths/makevalid/main.go:221
72 tegola/maths/makevalid/main.go:295
73 tegola/maths/makevalid/main.go:504
74 tegola/maths/makevalid/makevalid.go:77
75 tegola/maths/makevalid/makevalid.go:89
76 tegola/maths/makevalid/makevalid.go:118
77 tegola/maths/makevalid/makevalid_test.go:93
78 tegola/maths/makevalid/makevalid_test.go:163
79 tegola/maths/makevalid/plyg/ring.go:518
80 tegola/maths/triangle.go:1023
81 tegola/mvt/layer.go:73
82 tegola/mvt/layer.go:79
83 tegola/mvt/vector_tile/vector_tile.pb.go:64
84 tegola/provider/gpkg/gpkg.go:138
85 tegola/provider/gpkg/gpkg.go:223
86 tegola/provider/gpkg/gpkg_register.go:46
87 tegola/provider/gpkg/gpkg_register.go:51
88 tegola/provider/gpkg/gpkg_register.go:186
89 tegola/provider/gpkg/gpkg_register.go:227
90 tegola/provider/gpkg/gpkg_register.go:240
91 tegola/provider/gpkg/gpkg_register.go:245
92 tegola/provider/gpkg/gpkg_register.go:256
93 tegola/provider/gpkg/gpkg_register.go:377
94 tegola/provider/postgis/postgis.go:112
95 tegola/provider/postgis/postgis.go:117
96 tegola/provider/postgis/postgis.go:122
97 tegola/provider/postgis/postgis.go:127
98 tegola/provider/postgis/postgis.go:136
99 tegola/provider/postgis/postgis.go:142
100 tegola/provider/postgis/postgis.go:148
101 tegola/provider/postgis/postgis.go:153
102 tegola/provider/postgis/postgis.go:158
103 tegola/provider/postgis/postgis.go:163
104 tegola/provider/postgis/postgis.go:181
105 tegola/provider/postgis/postgis.go:198
106 tegola/provider/postgis/postgis.go:264
107 tegola/provider/postgis/postgis.go:441
108 tegola/provider/postgis/postgis.go:446
109 tegola/provider/postgis/postgis.go:529
110 tegola/provider/postgis/postgis.go:559
111 tegola/provider/postgis/postgis.go:603
112 tegola/provider/postgis/util.go:31
113 tegola/provider/postgis/util.go:36
114 tegola/provider/postgis/util.go:200
115 tegola/server/bindata/bindata.go:89
116 tegola/server/bindata/bindata.go:109
117 tegola/server/bindata/bindata.go:129
118 tegola/server/bindata/bindata.go:149
119 tegola/server/bindata/bindata.go:169
120 tegola/server/bindata/bindata.go:189
121 tegola/server/bindata/bindata.go:209
122 tegola/server/bindata/bindata.go:229
123 tegola/server/bindata/bindata.go:370
124 tegola/server/bindata/bindata.go:374
125 tegola/server/bindata/bindata.go:378
126 tegola/server/bindata/bindata.go:382
127 tegola/server/bindata/bindata.go:386
128 tegola/server/bindata/bindata.go:402
129 tegola/server/middleware_gzip.go:71
130 tegola/server/middleware_gzip.go:78
131 tegola/server/server_test.go:85
--- <err> name is different from "err" ---
1 tegola/basic/json_marshal.go:276
--- { return ... zero values ..., expr } ---
1 tegola/basic/geometry_math.go:214
2 tegola/basic/geometry_math.go:222
3 tegola/basic/geometry_math.go:230
4 tegola/cache/azblob/azblob.go:131
5 tegola/cache/azblob/azblob.go:140
6 tegola/cache/azblob/azblob.go:149
7 tegola/cache/azblob/azblob.go:171
8 tegola/cache/file/file.go:47
9 tegola/cache/s3/s3.go:92
10 tegola/cmd/internal/register/maps.go:108
11 tegola/cmd/tegola/cmd/cache/flags.go:20
12 tegola/cmd/tegola/cmd/cache/tile_name.go:51
13 tegola/cmd/tegola/cmd/cache/worker.go:112
14 tegola/cmd/tegola/cmd/cache/worker.go:123
15 tegola/cmd/tegola/cmd/root.go:73
16 tegola/cmd/tegola/cmd/root.go:78
17 tegola/cmd/xyz2svg/cmd/root.go:60
18 tegola/provider/gpkg/gpkg.go:90
19 tegola/provider/gpkg/gpkg.go:95
20 tegola/provider/gpkg/gpkg_register.go:264
21 tegola/provider/gpkg/gpkg_register.go:297
22 tegola/provider/gpkg/gpkg_register.go:302
23 tegola/provider/gpkg/gpkg_register.go:313
24 tegola/provider/gpkg/gpkg_register.go:328
25 tegola/provider/postgis/postgis.go:193
26 tegola/provider/postgis/postgis.go:208
27 tegola/provider/postgis/postgis.go:222
28 tegola/provider/postgis/postgis.go:228
29 tegola/provider/postgis/postgis.go:234
30 tegola/provider/postgis/postgis.go:243
31 tegola/provider/postgis/postgis.go:249
32 tegola/provider/postgis/postgis.go:255
33 tegola/provider/postgis/postgis.go:304
34 tegola/provider/postgis/postgis.go:315
35 tegola/provider/postgis/postgis.go:319
36 tegola/provider/postgis/postgis.go:364
37 tegola/provider/postgis/postgis.go:456
38 tegola/provider/postgis/postgis.go:520
39 tegola/provider/postgis/postgis.go:534
40 tegola/provider/postgis/postgis.go:565
41 tegola/provider/postgis/util.go:108
42 tegola/provider/postgis/util.go:113
43 tegola/server/bindata/bindata.go:29
44 tegola/server/bindata/bindata.go:245
45 tegola/server/bindata/bindata.go:271
46 tegola/server/bindata/bindata.go:396
--- single statement then branch ---
1 tegola/cache/azblob/azblob.go:241
2 tegola/cache/file/file.go:87
3 tegola/cache/s3/s3.go:321
4 tegola/cmd/internal/register/caches.go:18
5 tegola/cmd/internal/register/providers.go:43
6 tegola/cmd/internal/register/providers.go:62
7 tegola/cmd/internal/register/providers.go:75
8 tegola/config/config.go:192
9 tegola/config/config.go:207
10 tegola/config/config.go:217
11 tegola/internal/env/dict.go:43
12 tegola/internal/env/dict.go:121
13 tegola/internal/env/dict.go:197
14 tegola/internal/env/dict.go:273
15 tegola/internal/env/dict.go:348
16 tegola/internal/env/parse.go:79
17 tegola/internal/env/parse.go:126
18 tegola/internal/env/parse.go:184
19 tegola/internal/env/parse.go:231
20 tegola/maths/makevalid/plyg/ring.go:541
21 tegola/maths/maths.go:239
22 tegola/maths/validate/validate.go:49
23 tegola/maths/validate/validate.go:53
24 tegola/maths/validate/validate.go:59
25 tegola/maths/validate/validate.go:69
26 tegola/mvt/feature.go:94
27 tegola/mvt/feature.go:99
28 tegola/mvt/feature.go:592
29 tegola/mvt/feature.go:603
30 tegola/mvt/layer.go:90
31 tegola/mvt/tile.go:48
32 tegola/provider/postgis/postgis.go:570
33 tegola/provider/postgis/postgis.go:586
34 tegola/tile.go:172
--- complex then branch; cannot use try ---
1 tegola/cache/azblob/azblob.go:226
2 tegola/cache/file/file.go:78
3 tegola/cache/file/file.go:122
4 tegola/cache/s3/s3.go:195
5 tegola/cache/s3/s3.go:206
6 tegola/cache/s3/s3.go:219
7 tegola/cache/s3/s3.go:307
8 tegola/provider/gpkg/gpkg.go:39
9 tegola/provider/gpkg/gpkg.go:45
10 tegola/provider/gpkg/gpkg.go:131
11 tegola/provider/gpkg/gpkg.go:154
12 tegola/provider/gpkg/gpkg_register.go:171
13 tegola/provider/gpkg/gpkg_register.go:195
--- stats ---
1294 (100.0% of 1294) func declarations
246 ( 19.0% of 1294) func declarations returning an error
2693 (100.0% of 2693) statements
551 ( 20.5% of 2693) if statements
238 ( 43.2% of 551) if <err> != nil statements
131 ( 55.0% of 238) try candidates
1 ( 0.4% of 238) <err> name is different from "err"
--- non-try candidates ---
46 ( 19.3% of 238) { return ... zero values ..., expr }
34 ( 14.3% of 238) single statement then branch
13 ( 5.5% of 238) complex then branch; cannot use try
0 ( 0.0% of 238) non-empty else branch; cannot use try
And the companion project: (http://github.com/go-spatial/geom)
--- try candidates ---
1 geom/bbox.go:202
2 geom/encoding/geojson/geojson.go:152
3 geom/encoding/geojson/geojson.go:157
4 geom/encoding/wkb/internal/tcase/symbol/symbol.go:73
5 geom/encoding/wkb/internal/tcase/tcase.go:161
6 geom/encoding/wkb/internal/tcase/tcase.go:172
7 geom/encoding/wkb/wkb.go:50
8 geom/encoding/wkb/wkb.go:110
9 geom/encoding/wkt/internal/token/token.go:176
10 geom/encoding/wkt/internal/token/token.go:252
11 geom/internal/parsing/parsing.go:44
12 geom/internal/parsing/parsing.go:85
13 geom/internal/rtreego/rtree_test.go:110
14 geom/multi_line_string.go:34
15 geom/multi_polygon.go:35
16 geom/planar/clip/linestring.go:82
17 geom/planar/clip/linestring.go:181
18 geom/planar/clip/point.go:23
19 geom/planar/intersect/xsweep.go:106
20 geom/planar/makevalid/makevalid.go:92
21 geom/planar/makevalid/makevalid.go:191
22 geom/planar/makevalid/setdiff/polygoncleaner.go:283
23 geom/planar/makevalid/setdiff/polygoncleaner.go:345
24 geom/planar/makevalid/setdiff/polygoncleaner.go:543
25 geom/planar/makevalid/setdiff/polygoncleaner.go:554
26 geom/planar/makevalid/setdiff/polygoncleaner.go:572
27 geom/planar/makevalid/setdiff/polygoncleaner.go:578
28 geom/planar/simplify/douglaspeucker.go:84
29 geom/planar/simplify/douglaspeucker.go:88
30 geom/planar/simplify.go:13
31 geom/planar/triangulate/constraineddelaunay/triangle.go:186
32 geom/planar/triangulate/constraineddelaunay/triangulator.go:134
33 geom/planar/triangulate/constraineddelaunay/triangulator.go:138
34 geom/planar/triangulate/constraineddelaunay/triangulator.go:142
35 geom/planar/triangulate/constraineddelaunay/triangulator.go:173
36 geom/planar/triangulate/constraineddelaunay/triangulator.go:176
37 geom/planar/triangulate/constraineddelaunay/triangulator.go:203
38 geom/planar/triangulate/constraineddelaunay/triangulator.go:248
39 geom/planar/triangulate/constraineddelaunay/triangulator.go:396
40 geom/planar/triangulate/constraineddelaunay/triangulator.go:466
41 geom/planar/triangulate/constraineddelaunay/triangulator.go:553
42 geom/planar/triangulate/constraineddelaunay/triangulator.go:583
43 geom/planar/triangulate/constraineddelaunay/triangulator.go:667
44 geom/planar/triangulate/constraineddelaunay/triangulator.go:672
45 geom/planar/triangulate/constraineddelaunay/triangulator.go:677
46 geom/planar/triangulate/constraineddelaunay/triangulator.go:814
47 geom/planar/triangulate/constraineddelaunay/triangulator.go:818
48 geom/planar/triangulate/constraineddelaunay/triangulator.go:823
49 geom/planar/triangulate/constraineddelaunay/triangulator.go:865
50 geom/planar/triangulate/constraineddelaunay/triangulator.go:870
51 geom/planar/triangulate/constraineddelaunay/triangulator.go:875
52 geom/planar/triangulate/constraineddelaunay/triangulator.go:897
53 geom/planar/triangulate/constraineddelaunay/triangulator.go:901
54 geom/planar/triangulate/constraineddelaunay/triangulator.go:907
55 geom/planar/triangulate/constraineddelaunay/triangulator.go:1107
56 geom/planar/triangulate/constraineddelaunay/triangulator.go:1146
57 geom/planar/triangulate/constraineddelaunay/triangulator.go:1157
58 geom/planar/triangulate/constraineddelaunay/triangulator.go:1202
59 geom/planar/triangulate/constraineddelaunay/triangulator.go:1206
60 geom/planar/triangulate/constraineddelaunay/triangulator.go:1216
61 geom/planar/triangulate/delaunaytriangulationbuilder.go:66
62 geom/planar/triangulate/incrementaldelaunaytriangulator.go:46
63 geom/planar/triangulate/incrementaldelaunaytriangulator.go:78
64 geom/planar/triangulate/quadedge/lastfoundquadedgelocator.go:65
65 geom/planar/triangulate/quadedge/quadedgesubdivision.go:976
66 geom/slippy/tile.go:133
--- { return ... zero values ..., expr } ---
1 geom/internal/parsing/parsing.go:125
2 geom/planar/triangulate/constraineddelaunay/triangulator.go:428
3 geom/planar/triangulate/constraineddelaunay/triangulator.go:447
4 geom/planar/triangulate/constraineddelaunay/triangulator.go:460
--- single statement then branch ---
1 geom/bbox.go:259
2 geom/encoding/wkb/internal/decode/decode.go:29
3 geom/encoding/wkb/internal/decode/decode.go:55
4 geom/encoding/wkb/internal/decode/decode.go:63
5 geom/encoding/wkb/internal/decode/decode.go:70
6 geom/encoding/wkb/internal/decode/decode.go:79
7 geom/encoding/wkb/internal/decode/decode.go:84
8 geom/encoding/wkb/internal/decode/decode.go:93
9 geom/encoding/wkb/internal/decode/decode.go:99
10 geom/encoding/wkb/internal/decode/decode.go:105
11 geom/encoding/wkb/internal/decode/decode.go:114
12 geom/encoding/wkb/internal/decode/decode.go:119
13 geom/encoding/wkb/internal/decode/decode.go:135
14 geom/encoding/wkb/internal/decode/decode.go:140
15 geom/encoding/wkb/internal/decode/decode.go:149
16 geom/encoding/wkb/internal/decode/decode.go:155
17 geom/encoding/wkb/internal/decode/decode.go:161
18 geom/encoding/wkb/internal/decode/decode.go:170
19 geom/encoding/wkb/internal/decode/decode.go:176
20 geom/encoding/wkb/internal/tcase/token/token.go:162
21 geom/encoding/wkt/internal/token/token.go:136
--- complex then branch; cannot use try ---
1 geom/encoding/wkb/internal/tcase/tcase.go:74
2 geom/encoding/wkt/internal/symbol/symbol.go:125
3 geom/planar/intersect/xsweep.go:165
4 geom/planar/makevalid/makevalid.go:85
5 geom/planar/makevalid/makevalid.go:172
6 geom/planar/makevalid/triangulate.go:19
7 geom/planar/makevalid/triangulate.go:28
8 geom/planar/makevalid/triangulate.go:36
9 geom/planar/makevalid/triangulate.go:58
10 geom/planar/triangulate/constraineddelaunay/triangulator.go:358
11 geom/planar/triangulate/constraineddelaunay/triangulator.go:373
12 geom/planar/triangulate/constraineddelaunay/triangulator.go:453
13 geom/planar/triangulate/constraineddelaunay/triangulator.go:1237
14 geom/planar/triangulate/constraineddelaunay/triangulator.go:1243
15 geom/planar/triangulate/constraineddelaunay/triangulator.go:1249
--- stats ---
820 (100.0% of 820) func declarations
146 ( 17.8% of 820) func declarations returning an error
1715 (100.0% of 1715) statements
391 ( 22.8% of 1715) if statements
111 ( 28.4% of 391) if <err> != nil statements
66 ( 59.5% of 111) try candidates
0 ( 0.0% of 111) <err> name is different from "err"
--- non-try candidates ---
4 ( 3.6% of 111) { return ... zero values ..., expr }
21 ( 18.9% of 111) single statement then branch
15 ( 13.5% of 111) complex then branch; cannot use try
0 ( 0.0% of 111) non-empty else branch; cannot use try
On the subject of unexpected costs, I repost this from #32611...
I see three classes of cost:
Re nos. 1 & 2, the costs of try()
are modest.
To oversimplify no. 3, most commenters believe try()
would damage our code and/or the ecosystem of code we depend on, and thereby reduce our productivity and quality of product. This widespread, well-reasoned perception should not be disparaged as "non-factual" or "aesthetic".
The cost to the ecosystem is far more important than the cost to the spec or to tooling.
@griesemer it's patently unfair to claim that "three dozen vocal opponents" are the bulk of the opposition. Hundreds of people have commented here and in #32825. You told me on June 12, "I recognize that about 2/3 of the respondents are not happy with the proposal." Since then over 2,000 people have voted on "leave err != nil
alone" with 90% thumb-up.
@gdey could you amend your post to include only _stats & non-try candidates_ ?
@robfig, @gdey Thanks for providing this data, especially the before/after comparison.
@griesemer
You have certainly added some substance clarifying that my (and others') concerns might be addressed directly. My question, then, is whether the Go team does see the likely abuse of the indirect modes (i.e. naked returns and/or post-function scope error mutation via defer) as a cost worth discussing during step 5, and that it is worth potentially taking action toward it's mitigation. The current mood is that this most disconcerting aspect of the proposal is seen as a clever/novel feature by the Go team (This concern is not addressed by the assessment of the automated transformations and seems to be actively encouraged/supported. - errd
, in conversation, etc.).
edit to add... The concern with the Go team encouraging what veteran Gophers see as prohibitive is what I meant regarding tone-deafness.
... Indirection is a cost that many of us are deeply concerned about as a matter of experiential pain. It may not be something that can be benchmarked easily (if at all reasonably), but it is disingenuous to regard this concern as sentimental itself. Rather, disregarding the wisdom of shared experience in favor of simple numbers without solid contextual judgment is the sort of sentiment I/we are trying to work against.
@networkimprov Apologies for being not clear enough. What I did say was:
There are perhaps two, maybe three dozen vocal opponents of this proposal - you know who you are. They are dominating this discussion with frequent posts, sometimes multiple a day.
I was talking about actual comments (as in "frequent posts"), not emojis. There is only a relatively small number of people posting here _repeatedly_, which I believe is still correct. I was also not talking about #32825; I was talking about this proposal.
Looking at emojis, the situation is virtually unchanged from a month ago: 1/3 of emojis indicate a favorable opinion, and 2/3 indicate a negative opinion.
@griesemer
I remembered something while writing my comment above: while the design doc says that try
can be implemented as a straightforward syntax tree transformation, and in many cases that is obviously the case, there are some instances where I don't see a straightforward way of doing it. For instance, suppose we have the following:
switch x {
case rand.Int():
a()
case 5, try(strconv.Atoi(y)):
b()
}
Given the evaluation order of switch
, I don't see how to trivially lift the strconv.Atoi(y)
out of the case
clause while preserving the intended semantics; the best I could come up with is to rewrite the switch
as the equivalent chain of if
/else
statements, like this:
if x == rand.Int() {
a()
} else if x == 5 {
b()
} else if _v, _err := strconv.Atoi(y); _err != nil {
return _err
} else if x == _v {
b()
}
(There are other situations where this can come up, but this is one of the simplest examples and the first that comes to mind.)
In fact, before you published this proposal I'd been working on an AST translator to implement the check
operator from the design draft and ran into this problem. However, I was using a hacked version of the go/*
stdlib packages; perhaps the compiler front-end is structured in a way that makes this easier? Or have I missed something and there really is a straightforward way to do this?
See also https://github.com/rhysd/trygo; according to the README it does not implement try
expressions, and notes essentially the same concern I'm raising here; I suspect that may be why the author didn't implement that feature.
@daved Professional code is not being developed in a vacuum - there are local conventions, style recommendations, code reviews, etc. (I've said this before). Thus, I don't see why abuse would be "likely" (it's possible, but that's true for any language construct).
Note that using defer
to decorate errors is possible with or without try
. There are certainly good reason for a function that contains many error checks, all of which decorate errors the same way, to do that decoration once, for instance using a defer
. Or maybe use a wrapper function that does the decoration. Or any other mechanism that fits the bill and the local coding recommendations. After all, "errors are just values" and it totally makes sense to write and factor code that deals with errors.
Naked returns can be problematic when used in an undisciplined way. That doesn't mean they are generally bad. For instance, if a function's results are valid only if there was no error, it seems perfectly fine to use a naked return in the case of an error - as long as we are disciplined with setting the error (as the other return values don't matter in this case). try
ensures exactly that. I don't see any "abuse" here.
@dpinela The compiler already translates a switch
statement such as yours as a sequence of if-else-if
, so I don't see a problem here. Also, the "syntax tree" that the compiler is using is not the "go/ast" syntax tree. The compiler's internal representation allows for much more flexible code that can't necessarily be translated back into Go.
@griesemer
Yes, of course, all of what you're saying has basis. The gray area, however, is not as simplistic as you're framing it to be. Naked returns are normally treated with great caution by those of us who teach others (we, who strive to grow/promote the community). I appreciate that the stdlib has it littered throughout. But, when teaching others, explicit returns is always emphasized. Let the individual reach their own maturity to turn to the more "fanciful" approach, but encouraging it from the start would surely be fostering hard-to-read code (i.e. bad habits). This, again, is the tone-deafness I'm trying to bring to light.
Personally, I do not wish to forbid naked returns or deferred value manipulation. When they are truly suitable I am glad these capabilities are available (though, other experienced users may take a more rigid stance). Nonetheless, encouraging the application of these less common and generally fragile features in such a pervasive manner is thoroughly the opposite direction I ever imagined Go taking. Is the pronounced change in character of eschewing magic and precarious forms of indirection a purposed shift? Should we also start emphasizing the use of DICs and other hard-to-debug mechanisms?
p.s. Your time is greatly appreciated. Your team and the language has my respect, and care. I don't wish any grief for anyone in speaking out; I hope you will hear the nature of my/our concern and try to see things from our "front-lines" perspective.
Adding a few comments to my downvote.
For the specific proposal at hand:
1) I would greatly prefer this to be a keyword vs. a built-in function for previously articulated reasons of control flow and code readability.
2) Semantically, "try" is a lightning rod. And, unless there is an exception thrown, "try" would be better renamed to something like guard
or ensure
.
3) Besides these two points, I think this is the best proposal I've seen for this sort of thing.
A couple more comments articulating my objection to any addition of a try/guard/ensure
concept vs. leaving if err != nil
alone:
1) This runs counter to one of golang's original mandates (at least as I perceived it) to be explicit, easy to read/understand, with very little 'magic'.
2) This will encourage laziness at the precise moment when thought is required: "what is the best thing for my code to do in the case of this error?". There are many errors that can arise while doing "boilerplate" things like opening files, transferring data over a network, etc. While you may start out with a bunch of "trys" that ignore non-common failure scenarios, eventually many of these "trys" will go away as you may need to implement your own backoff/retry, logging/tracing, and/or cleanup tasks. “Low-probability events” are guaranteed at scale.
Here are some more raw tryhard
stats. This is only lightly validated, so feel free to point out errors. ;-)
These are the repositories that correspond to the first 20 Popular Packages on https://godoc.org, sorted by try candidate percentage. This is using the default tryhard
settings, which in theory should be excluding vendor
directories.
The median value for try candidates across these 20 repos is 58%.
| project | loc | if stmts | if != nil (% of if) | try candidates (% of if != nil) |
|---------|-----|---------------|----------------------|---------------
| github.com/google/uuid | 1714 | 12 | 16.7% | 0.0% |
| github.com/pkg/errors | 1886 | 10 | 0.0% | 0.0% |
| github.com/aws/aws-sdk-go | 1911309 | 32015 | 9.4% | 8.9% |
| github.com/jinzhu/gorm | 15246 | 44 | 11.4% | 20.0% |
| github.com/robfig/cron | 1911 | 20 | 35.0% | 28.6% |
| github.com/gorilla/websocket | 6959 | 212 | 32.5% | 39.1% |
| github.com/dgrijalva/jwt-go | 3270 | 118 | 29.7% | 40.0% |
| github.com/gomodule/redigo | 7119 | 187 | 34.8% | 41.5% |
| github.com/unixpickle/kahoot-hack | 1743 | 52 | 75.0% | 43.6% |
| github.com/lib/pq | 13396 | 239 | 30.1% | 55.6% |
| github.com/sirupsen/logrus | 5063 | 29 | 17.2% | 60.0% |
| github.com/prometheus/client_golang | 17791 | 194 | 49.0% | 62.1% |
| github.com/go-redis/redis | 21182 | 326 | 42.6% | 73.4% |
| github.com/mongodb/mongo-go-driver | 86605 | 2097 | 37.8% | 73.9% |
| github.com/uber-go/zap | 15363 | 84 | 36.9% | 74.2% |
| github.com/golang/protobuf | 42959 | 685 | 22.9% | 77.1% |
| github.com/gin-gonic/gin | 14574 | 96 | 53.1% | 86.3% |
| github.com/go-pg/pg | 26369 | 831 | 37.7% | 86.9% |
| github.com/Shopify/sarama | 36427 | 1369 | 68.2% | 91.0% |
| github.com/stretchr/testify | 13496 | 32 | 43.8% | 92.9% |
The "if stmts" column is only tallying if
statements in functions returning an error, which is how tryhard
reports it, and which hopefully explains why it is so low for something like gorm
.
Given popular packages on godoc.org tend to be library packages, I wanted to also check stats for some larger projects as well.
These are misc. large projects that happened to be top-of-mind for me (i.e., no real logic behind these 10). This is again sorted by try candidate percentage.
The median value for try candidates across these 10 repos is 59%.
| project | loc | if stmts | if != nil (% of if) | try candidates (% of if != nil) |
|---------|-----|---------------|----------------------|---------------------------------|
| github.com/juju/juju | 1026473 | 26904 | 51.9% | 17.5% |
| github.com/go-kit/kit | 38949 | 467 | 57.0% | 51.9% |
| github.com/boltdb/bolt | 12426 | 228 | 46.1% | 53.3% |
| github.com/hashicorp/consul | 249369 | 5477 | 47.6% | 54.5% |
| github.com/docker/docker | 251152 | 8690 | 48.7% | 56.8% |
| github.com/istio/istio | 429636 | 7564 | 40.4% | 61.9% |
| github.com/gohugoio/hugo | 94875 | 1853 | 42.4% | 64.8% |
| github.com/etcd-io/etcd | 209603 | 4657 | 38.3% | 65.5% |
| github.com/kubernetes/kubernetes | 1789172 | 40289 | 43.3% | 66.5% |
| github.com/cockroachdb/cockroach | 1038529 | 22018 | 39.9% | 74.0% |
These two tables of course only represent a sample of open source projects, and only reasonably well known ones. I've seen people theorize that private code bases would show greater diversity, and there is at least some evidence of that based on some of the numbers that various people have been posting.
@thepudds, that doesn't look like the most recent _tryhard_, which gives "non-try candidates".
@networkimprov I can confirm that at least for gorm
these are results from the latest tryhard
. The "non-try candidates" are simply not reported in the tables above.
@daved First, let me assure you that I/we hear you loud and clear. Though we're still early in the process and lots of things can change. Let's not jump the gun.
I understand (and appreciate) that one might want to chose a more conservative approach when teaching Go. Thanks.
@griesemer FYI here are the results of running that latest version of tryhard on 233k lines of code I've been involved in, much of it not open source:
--- stats ---
8760 (100.0% of 8760) functions (function literals are ignored)
2942 ( 33.6% of 8760) functions returning an error
22991 (100.0% of 22991) statements in functions returning an error
5548 ( 24.1% of 22991) if statements
2929 ( 52.8% of 5548) if <err> != nil statements
163 ( 5.6% of 2929) try candidates
0 ( 0.0% of 2929) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
2213 ( 75.6% of 2929) { return ... zero values ..., expr }
167 ( 5.7% of 2929) single statement then branch
253 ( 8.6% of 2929) complex then branch; cannot use try
14 ( 0.5% of 2929) non-empty else branch; cannot use try
Much of the code uses an idiom similar to:
if err != nil {
return ... zero values ..., errors.Wrap(err)
}
It might be interesting if tryhard
could identify when all such expressions in a function use an identical expression - i.e. when it might be possible to rewrite the function with a single common defer
handler.
Here are the statistics for a small GCP helper tool to automate user and project creation:
$ tryhard -r .
--- stats ---
129 (100.0% of 129) functions (function literals are ignored)
75 ( 58.1% of 129) functions returning an error
725 (100.0% of 725) statements in functions returning an error
164 ( 22.6% of 725) if statements
93 ( 56.7% of 164) if <err> != nil statements
64 ( 68.8% of 93) try candidates
0 ( 0.0% of 93) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
17 ( 18.3% of 93) { return ... zero values ..., expr }
7 ( 7.5% of 93) single statement then branch
1 ( 1.1% of 93) complex then branch; cannot use try
0 ( 0.0% of 93) non-empty else branch; cannot use try
After this, I went ahead and checked all the places in the code that are still dealing with an err
variable to see if I could find any meaningful patterns.
err
sIn a couple of places, we do't want to stop execution on the first error and instead be able to see all the errors that occurred once at the end of the run. Maybe there is a different way of doing this that integrates well with try
or some form of support for multi-errors is added to Go itself.
var errs []error
for _, p := range toDelete {
fmt.Println("delete:", p.ProjectID)
if err := s.DeleteProject(ctx, p.ProjectID); err != nil {
errs = append(errs, err)
}
}
After having read this comment again, there were suddenly a lot of potential try
cases that jumped to my attention. They are all similar in that the calling function is decorating the error of a called function with information that the called function could already have added to the error:
func run() error {
key := "MY_ENV_VAR"
client, err := ClientFromEnvironment(key)
if err != nil {
// "github.com/pkg/errors"
return errors.Wrap(err, key)
}
// do something with `client`
}
func ClientFromEnvironment(key string) (*http.Client, error) {
filename, ok := os.LookupEnv(key)
if !ok {
return nil, errors.New("environment variable not set")
}
return ClientFromFile(filename)
}
Quoting the important part from the Go blog here again here for clarity:
It is the error implementation's responsibility to summarize the context. The error returned by os.Open formats as "open /etc/passwd: permission denied," not just "permission denied." The error returned by our Sqrt is missing information about the invalid argument.
With this in mind, the above code now becomes:
func run() error {
key := "MY_ENV_VAR"
client := try(ClientFromEnvironment(key))
// do something with `client`
}
func ClientFromEnvironment(key string) (*http.Client, error) {
filename, ok := os.LookupEnv(key)
if !ok {
return nil, fmt.Errorf("environment variable not set: %s", key)
}
return ClientFromFile(filename)
}
At first glance, this seems like a minor change but in my estimation, it could mean that try
is actually incentivising to push better and more consistent error handling up the function chain and closer to the source or package.
Overall, I think the value that try
is bringing long-term is higher than the potential issues that I currently see with it, which are:
try
is changing control flow.try
means you can no longer put a debug stopper in the return err
case.Since those concerns are already known to the Go team, I am curious to see how these will play out in "the real world". Thanks for your time in reading and responding to all of our messages.
Fixed a function signature that didn't return an error
before. Thank you @magical for spotting that!
func main() { key := "MY_ENV_VAR" client := try(ClientFromEnvironment(key)) // do something with `client` }
@mrkanister Nitpicking, but you can't actually use try
in this example because main
does not return an error
.
This is an appreciation comment;
thanks @griesemer for the gardening and all, that you have been doing on this issue as well as elsewhere.
In case you have many lines like these (from https://github.com/golang/go/issues/32437#issuecomment-509974901):
if !ok {
return nil, fmt.Errorf("environment variable not set: %s", key)
}
You could use a helper function that only returns a non-nil error if some condition is true:
try(condErrorf(!ok, "environment variable not set: %s", key))
Once common patterns are identified, I think it will be possible to handle many of them with only a few helpers, first at the package level, and maybe eventually reaching the standard library. Tryhard is great, it is doing a wonderful job and giving lots of interesting information, but there is much more.
As an addition to the single-line if proposal by @zeebo and others, the if statement could have a compact form that removes the != nil
and the curly braces:
if err return err
if err return errors.Wrap(err, "foo: failed to boo")
if err return fmt.Errorf("foo: failed to boo: %v", err)
I think this is simple, lightweight & readable. There are two parts:
if variable return ...
. Since the return
is so close to the left-hand-side it seems to be still quite easy to skim the code - the extra difficulty of doing so being one of the main arguments against single-line ifs (?) Go also already has precedent for simplifying syntax by for example removing parentheses from its if statement.Current style:
a, err := BusinessLogic(state)
if err != nil {
return nil, err
}
One-line if:
a, err := BusinessLogic(state)
if err != nil { return nil, err }
One-line compact if:
a, err := BusinessLogic(state)
if err return nil, err
a, err := BusinessLogic(state)
if err return nil, errors.Wrap(err, "some context")
func (c *Config) Build() error {
pkgPath, err := c.load()
if err return nil, errors.WithMessage(err, "load config dir")
b := bytes.NewBuffer(nil)
err = templates.ExecuteTemplate(b, "main", c)
if err return nil, errors.WithMessage(err, "execute main template")
buf, err := format.Source(b.Bytes())
if err return nil, errors.WithMessage(err, "format main template")
target := fmt.Sprintf("%s.go", filename(pkgPath))
err = ioutil.WriteFile(target, buf, 0644)
if err return nil, errors.WithMessagef(err, "write file %s", target)
// ...
}
@eug48 see #32611
Here are tryhard stats for a monorepo (lines of go code, excluding vendor'd code: 2,282,731):
--- stats ---
117551 (100.0% of 117551) functions (function literals are ignored)
35726 ( 30.4% of 117551) functions returning an error
263725 (100.0% of 263725) statements in functions returning an error
50690 ( 19.2% of 263725) if statements
25042 ( 49.4% of 50690) if <err> != nil statements
12091 ( 48.3% of 25042) try candidates
36 ( 0.1% of 25042) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
3561 ( 14.2% of 25042) { return ... zero values ..., expr }
3304 ( 13.2% of 25042) single statement then branch
4966 ( 19.8% of 25042) complex then branch; cannot use try
296 ( 1.2% of 25042) non-empty else branch; cannot use try
Given that people are still proposing alternatives, I'd like to know in more detail what functionality it is that the broader Go community actually wants from any proposed new error handling feature.
I've put together a survey listing a bunch of different features, pieces of error handling functionality I've seen people propose. I've carefully _omitted any proposed naming or syntax_, and of course tried to make the survey neutral rather than favoring my own opinions.
If people would like to participate, here's the link, shortened for sharing:
https://forms.gle/gaCBgxKRE4RMCz7c7
Everyone who participates should be able to see the summary results. Perhaps this might help focus the discussion?
if err := os.Setenv("GO111MODULE", "on"); err != nil {
return err
}
The defer handler adding context doesn't work in this case or does it? If not then it would be nice to make it more visible, if possible since it happens quite fast, especially since that's the standard go-to until now.
Oh, and please introduce try
, found lot's of use cases here too.
--- stats ---
929 (100.0% of 929) functions (function literals are ignored)
230 ( 24.8% of 929) functions returning an error
1480 (100.0% of 1480) statements in functions returning an error
320 ( 21.6% of 1480) if statements
206 ( 64.4% of 320) if <err> != nil statements
109 ( 52.9% of 206) try candidates
2 ( 1.0% of 206) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
53 ( 25.7% of 206) { return ... zero values ..., expr }
18 ( 8.7% of 206) single statement then branch
17 ( 8.3% of 206) complex then branch; cannot use try
2 ( 1.0% of 206) non-empty else branch; cannot use try
@lpar You're welcome to discuss alternatives but please don't do this in this issue. This is about the try
proposal. The best place would actually be one of the mailing lists, e.g. go-nuts. The issue tracker is really best for tracking and discussing a specific issue rather than a general discussion. Thanks.
@fabstu The defer
handler will work in your example just fine, both with and without try
. Expanding your code with enclosing function:
func f() (err error) {
defer func() {
if err != nil {
err = decorate(err, "msg") // here you can modify the result error as you please
}
}()
...
if err := os.Setenv("GO111MODULE", "on"); err != nil {
return err
}
...
}
(note that the result err
will be set by the return err
; and the err
used by the return
is the one declared locally with the if
- these are just the normal scoping rules in action).
Or, using a try
, which will eliminate the need for the local err
variable:
func f() (err error) {
defer func() {
if err != nil {
err = decorate(err, "msg") // here you can modify the result error as you please
}
}()
...
try(os.Setenv("GO111MODULE", "on"))
...
}
And most probably, you'd want to use one of the proposed errors/errd
functions:
func f() (err error) {
defer errd.Wrap(&err, ... )
...
try(os.Setenv("GO111MODULE", "on"))
...
}
And if you don't need wrapping it will just be:
func f() error {
...
try(os.Setenv("GO111MODULE", "on"))
...
}
@fastu And finally, you can use errors/errd
also without try
and then you get:
func f() (err error) {
defer errd.Wrap(&err, ... )
...
if err := os.Setenv("GO111MODULE", "on"); err != nil {
return err
}
...
}
The more i think the more i like this proposal.
The only things that disturb me is to use named return everywhere. Is it finally a good practice and i should use it (never tried) ?
Anyway, before changing all my code, will it works like that ?
func f() error {
var err error
defer errd.Wrap(&err,...)
try(...)
}
@flibustenet Named result parameters by themselves are not a bad practice at all; the usual concern with named results is that they enable naked returns
; i.e., one can simply write return
w/o the need to specify the actual results _with the return
_. In general (but not always!) such practice makes it harder to read and reason about the code because one can't simply look at the return
statement and conclude what the result is. One has to scan the code for the result parameters. One may miss to set a result value, and so forth. So in some code bases, naked returns are simply discouraged.
But, as I mentioned before, if the results are invalid in case of an error, it's perfectly fine to set the error and ignore the rest. A naked return in such cases is perfectly ok as long as the error result is consistently set. try
will ensure exactly that.
Finally, named result parameters are only needed if you want to augment the error return with defer
. The design doc briefly also discusses the possibility to provide another built-in to access the error result. That would eliminate the need for named returns completely.
Regarding your code example: This will not work as expected because try
_always_ sets the _result error_ (which is unnamed in this case). But you are declaring a different local variable err
and the errd.Wrap
operates on that one. It won't be set by try
.
Quick experience report: I'm writing an HTTP request handler that looks like this:
func Handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := chi.URLParam(r, "id")
var err error
// starts as bad request, then it's an internal server error after we parse inputs
var statusCode = http.StatusBadRequest
defer func() {
if err != nil {
wrap := xerrors.Errorf("handler fail: %w", err)
logger.With(zap.Error(wrap)).Error("error")
http.Error(w, wrap.Error(), statusCode)
}
}()
var c Thingie
err = unmarshalBody(r, &c)
if err != nil {
return
}
statusCode = http.StatusInternalServerError
s, err := DoThing(ctx, c)
if err != nil {
return
}
d, err := DoThingWithResult(ctx, id, s)
if err != nil {
return
}
data, err := json.Marshal(detail)
if err != nil {
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_, err = w.Write(data)
if err != nil {
return
}
}
At first glance, it looks like this is an ideal candidate for try
since there's lots of error handling where there's nothing to do except return back a message, all of which can be done a defer. But you can't use try
because a request handler doesn't return error
. In order to use it, I'd have to wrap the body in a closure with the signature func() error
. That feels...inelegant and I suspect that code that looks like this is a somewhat common pattern.
@jonbodner
This works (https://play.golang.org/p/NaBZe-QShpu):
package main
import (
"errors"
"fmt"
"golang.org/x/xerrors"
)
func main() {
var err error
defer func() {
filterCheck(recover())
if err != nil {
wrap := xerrors.Errorf("app fail (at count %d): %w", ct, err)
fmt.Println(wrap)
}
}()
check(retNoErr())
n, err := intNoErr()
check(err)
n, err = intErr()
check(err)
check(retNoErr())
check(retErr())
fmt.Println(n)
}
func check(err error) {
if err != nil {
panic(struct{}{})
}
}
func filterCheck(r interface{}) {
if r != nil {
if _, ok := r.(struct{}); !ok {
panic(r)
}
}
}
var ct int
func intNoErr() (int, error) {
ct++
return 0, nil
}
func retNoErr() error {
ct++
return nil
}
func intErr() (int, error) {
ct++
return 0, errors.New("oops")
}
func retErr() error {
ct++
return errors.New("oops")
}
Ah, the first downvote! Good. Let the pragmatism flow through you.
Ran tryhard
on some of my codebases. Unfortunately, some of my packages have 0
try candidates despite being pretty large because the methods in them use a custom error implementation. For example, when building servers, I like for my business logic layer methods to only emit SanitizedError
s rather than error
s to ensure at compile time that things like filesystem paths or system info don't leak out to users in error messages.
For example, a method that uses this pattern might look something like this:
func (a *App) GetFriendsOfUser(userId model.Id) ([]*model.User, SanitizedError) {
if user, err := a.GetUserById(userId); err != nil {
// (*App).GetUserById returns (*model.User, SanitizedError)
// This could be a try() candidate.
return err
} else if user == nil {
return NewUserError("The specified user doesn't exist.")
}
friends, err := a.Store.GetFriendsOfUser(userId)
// (*Store).GetFriendsOfUser returns ([]*model.User, error)
// This could be a SQL error or a network error or who knows what.
return friends, NewInternalError(err)
}
Is there any reason why we can't relax the current proposal to work as long as the last return value of both the enclosing function and the try function expression implement error and are the same type? This would still avoid any concrete nil -> interface confusion, but it would enable try in situations like the above.
Thanks, @jonbodner, for your example. I'd write that code as follows (translation errors notwithstanding):
func Handler(w http.ResponseWriter, r *http.Request) {
statusCode, err := internalHandler(w, r)
if err != nil {
wrap := xerrors.Errorf("handler fail: %w", err)
logger.With(zap.Error(wrap)).Error("error")
http.Error(w, wrap.Error(), statusCode)
}
}
func internalHandler(w http.ResponseWriter, r *http.Request) (statusCode int, err error) {
ctx := r.Context()
id := chi.URLParam(r, "id")
// starts as bad request, then it's an internal server error after we parse inputs
statusCode = http.StatusBadRequest
var c Thingie
try(unmarshalBody(r, &c))
statusCode = http.StatusInternalServerError
s := try(DoThing(ctx, c))
d := try(DoThingWithResult(ctx, id, s))
data := try(json.Marshal(detail))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
try(w.Write(data))
return
}
It uses two functions but it's a lot shorter (29 lines vs 40 lines) - and I used nice spacing - and this code doesn't need a defer
. The defer
in particular, together with the statusCode being changed on the way down and used in the defer
makes the original code harder to follow than necessary. The new code, while it uses named results and a naked return (you can easily replace that with return statusCode, nil
if you want) is simpler because it cleanly separates error handling from the "business logic".
Just repost my comment in another issue https://github.com/golang/go/issues/32853#issuecomment-510340544
I think if we can provide another parameter funcname
, that will be great, otherwise we still don't know the error is returned by which function.
func foo() error {
handler := func(err error, funcname string) error {
return fmt.Errorf("%s: %v", funcname, err) // wrap something
//return nil // or dismiss
}
a, b := try(bar1(), handler)
c, d := try(bar2(), handler)
}
@ccbrown I wonder if your example would be amenable to the same treatment as above; i.e., if it would make sense to factor code such that internal errors are wrapped once (by an enclosing function) before they go out (rather than wrapping them everywhere). It would seem to me (w/o knowing much about your system) that that would be preferable as it would centralize the error wrapping in one place rather than everywhere.
But regarding your question: I'd have to think about making try
accept a more general error type (and return one as well). I don't see a problem with it at the moment (except that it's more complicated to explain) - but there may be an issue after all.
Along those lines, we have wondered early on if try
could be generalized so it doesn't just work for error
types, but any type, and the test err != nil
would then become x != zero
where x
is the equivalent of err
(the last result), and zero
the respective zero value for the type of x
. Unfortunately, this doesn't work for the common case of booleans (and pretty much any other basic type), because the zero value of bool
is false
and ok != false
is exactly the opposite of what we would want to test.
@lunny The proposed version of try
does not accept a handler function.
@griesemer Oh. What's a pity! Otherwise I can remove github.com/pkg/errors
and all errors.Wrap
.
@ccbrown I wonder if your example would be amenable to the same treatment as above; i.e., if it would make sense to factor code such that internal errors are wrapped once (by an enclosing function) before they go out (rather than wrapping them everywhere). It would seem to me (w/o knowing much about your system) that that would be preferable as it would centralize the error wrapping in one place rather than everywhere.
@griesemer Returning error
instead to an enclosing function would make it possible to forget to categorize each error as an internal error, user error, authorization error, etc. As-is, the compiler catches that, and using try
wouldn't be worth trading those compile-time checks for run-time checks.
I would like to say I like the design of try
, but there is still if
statement in the defer
handler while you use try
. I don't think that would be simpler than if
statements without try
and defer
handler. Maybe only using try
would be much better.
@ccbrown Got it. In retrospect, I think your suggested relaxation should be no problem. I believe we could relax try
to work with any interface type (and matching result type), for that matter, not just error
, as long as the relevant test remains x != nil
. Something to think about. This could be done early, or retro-actively as it would be a backward-compatible change I believe.
Is nobody bothered by this type of use of try:
data := try(json.Marshal(detail))
Regardless of the fact that the marshaling error can result in finding the correct line in the written code, I just feel uncomfortable knowing that this is a naked error being returned with no line number/caller information being included. Knowing the source file, function name, and line number is usually what I include when handling errors. Maybe I am misunderstanding something though.
@griesemer I wasn’t planning to discuss alternatives here. The fact that everyone keeps suggesting alternatives is exactly why I think a survey to find out what people actually want would be a good idea. I just posted about it here to try to catch as many people as possible who are interested in the possibility of improving Go error handling.
@trende-jp I really depends on the context of this line of code - all by itself it cannot be judged in any meaningful way. If this is the only call to json.Marshal
and you want to augment the error, an if
statement may be best. If there are lots of json.Marshal
calls, adding context to the error could be nicely done with a defer
; or perhaps by wrapping all these calls inside a local closure that returns the error. There's multitudes of ways how this could be factored if need be (i.e., if there are many such calls in the same function). "Errors are values" is true here as well: use code to make error handling manageable.
try
is not going to solve all your error handling problems - that's not the intent. It's simply another tool in the toolbox. And it's not really new machinery either, it's a form of syntactic sugar for a pattern that we have observed frequently over the course of almost a decade. We have some evidence that it would work really well in some code, and that it also would not be of much help in other code.
@trende-jp
Can it not be solved with defer
?
defer fmt.HandleErrorf(&err, "decoding %q", path)
Line numbers in error messages can also be solved as I've shown in my blog: How to use 'try'.
@trende-jp @faiface In addition to the line number, you could store the decorator string in a variable. This would let you isolate the specific function call that's failing.
I really think this absolutely should not be a built-in function.
It has been mentioned a few time that panic()
and recover()
also alter the control flow. Very well, let us not add more.
@networkimprov wrote https://github.com/golang/go/issues/32437#issuecomment-498960081:
It doesn't read like Go.
I couldn't agree more.
If anything, I believe any mechanism for addressing the root problem (and I'm not sure there is one), it should be triggered by a keyword (or key symbol ?).
How would you feel if go func()
were to be go(func())
?
How about use bang(!) instead of try
function. This could make functions chain possible:
func foo() {
f := os.Open!("")
defer f.Close()
// etc
}
func bar() {
count := mustErr!().Read!()
}
@sylr
How would you feel if go func() were to be go(func()) ?
Come on, that would be pretty acceptable.
@sylr Thanks, but we are not soliciting alternative proposals on this thread. See also this on staying focussed.
Regarding your comment: Go is a pragmatic language - using a built-in here is a pragmatic choice. It has several advantages over using a keyword as explained at length in the design doc. Note that try
is simply syntactic sugar for a common pattern (in contrast to go
which implements a major feature of Go and cannot be implemented with other Go mechanisms), like append
, copy
, etc. Using a built-in is a fine choice.
(But as I have said before, if _that_ is the only thing that prevents try
from being acceptable, we can consider making it a keyword.)
I was just pondering over a piece of my own code, and how that'd look with try
:
slurp, err := ioutil.ReadFile(path)
if err != nil {
return err
}
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)
Could become:
return ioutil.WriteFile(path, append(copyrightText, try(ioutil.ReadFile(path))...), 0666)
I'm not sure if this is better. It seems to make code much more difficult to read. But it might be just a matter of getting used to it.
@gbbr You have a choice here. You could write it as:
slurp := try(ioutil.ReadFile(path))
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)
which still saves you a lot of boilerplate yet makes it much clearer. This is not inherent to try
. Just because you can squeeze everything into a single expression doesn't mean you should. That applies generally.
@griesemer This example _is_ inherent to try, you may not nest code that may fail today - you are forced to handle errors with control flow. I would like to something cleared up from https://github.com/golang/go/issues/32825#issuecomment-507099786 / https://github.com/golang/go/issues/32825#issuecomment-507136111 to which you replied https://github.com/golang/go/issues/32825#issuecomment-507358397. Later the same issue was discussed again in https://github.com/golang/go/issues/32825#issuecomment-508813236 and https://github.com/golang/go/issues/32825#issuecomment-508937177 - the last of which I state:
Glad you read my central argument against try: the implementation is not restrictive enough. I believe that either the implementation should match all the proposals usage examples that are concise and easy to read.
_Or_ the proposal should contain examples that match the implementation so that all people considering it can be exposed to what will inevitably appear in Go code. Along with all the corner cases that we may face when troubleshooting less than ideally written software, which occurs in any language / environment. It should answer questions like what stack traces will look like with multiple nesting levels, are the locations of the errors easily recognizable? What about method values, anonymous function literals? What type of stack trace do the below produce if the line containing the calls to fn() fail?
fn := func(n int) (int, error) { ... } return try(func() (int, error) { mu.Lock() defer mu.Unlock() return try(try(fn(111111)) + try(fn(101010)) + try(func() (int, error) { // yea... })(2)) }(try(fn(1)))
I am well aware there will be lots of reasonable code written, but we are now providing a tool that has never existed before: the ability to potentially write code without clear control flow. So I want to justify why we even allow it in the first place, I never want my time wasted debugging this kind of code. Because I know I will, experience has taught me that someone will do it if you allow them. That someone is often an uninformed me.
Go provides the least possible ways for other developers and I to waste each-others time by limiting us to using the same mundane constructs. I don't want to lose that without an overwhelming benefit. I don't believe "because try is implemented as a function" to be an overwhelming benefit. Can you provide a reason why it is?
Having a stack trace that shows where the above fails would be useful, maybe adding a composite literal with fields that call that function into the mix? I am asking for this because I know how stack traces look today for this type of problem, Go does not provide easily digestible column information in the stack information only the hexadecimal function entry address. Several things worry me about this, such as stack trace consistency across architectures, for example consider this code:
package main
import "fmt"
func dopanic(b bool) int { if b { panic("panic") }; return 1 }
type bar struct { a, b, d int; b *bar }
func main() {
fmt.Println(&bar{
a: 1,
c: 1,
d: 1,
b: &bar{
a: 1,
c: 1,
d: dopanic(true) + dopanic(false),
},
})
}
Notice how the first playground fails at the left hand dopanic, the second on the right, yet both print an identical stack trace:
https://play.golang.org/p/SYs1r4hBS7O
https://play.golang.org/p/YMKkflcQuav
panic: panic
goroutine 1 [running]:
main.dopanic(...)
/tmp/sandbox709874298/prog.go:7
main.main()
/tmp/sandbox709874298/prog.go:27 +0x40
I would have expected the second one to be +0x41 or some offset after 0x40, which could be used to determine the actual call that failed within the panic. Even if we got the correct hexadecimal offsets, I won't be able to determine where the failure occurred without additional debugging. Today this is an edge case, something people will rarely face. If you release a nestable version of try it will become the norm, as even the proposal includes a try() + try() strconv showing it's both possible and acceptable to use try this way.
1) Given the information above, what changes to stack traces do you plan on making (if any) so I can still tell where my code failed?
2) Is try nesting allowed because you believe it should be? If so what do you see as the benefits for try nesting and how will you prevent abuse? I think tryhard should be adjusted to perform nested try's where you envision it as acceptable so people can make a more informed decision about how it impacts their code, since currently we are getting only best / strictest usage examples. This will give us an idea what type of vet
limitations will be imposed, as of right now you've said vet will be the defense against unreasonable try's, but how will that materialize?
3) Is try nesting because it happens to be a consequence of the implementation? If so doesn't this seem like a very weak argument for the most notable language change since Go was released?
I think this change needs more consideration around try nesting. Each time I think about it some some new pain point emerges somewhere, I am very worried that all the potential negatives won't emerge until it's revealed in the wild. Nesting also provides an easy way to leak resources as mentioned in https://github.com/golang/go/issues/32825#issuecomment-506882164 that isn't possible today. I think the "vet" story needs a much more concrete plan with examples of how it will provide feedback if it will be used as the defense against the harmful try() examples I've given here, or the implementation should provide compile time errors for usage outside your ideal best practices.
edit: I asked in gophers about play.golang.org architecture and someone mentioned it compiles via NaCl, so probably just a consequence / bug of that. But I could see this being a problem on other arch, I think a lot of the issues that could arise from introducing multiple returns per line just hasn't been fully explored since most of the usages center around sane & clean single line usage.
Oh no, please don't add this 'magic' into the langage.
These doesn't looks and feel like the rest of the langage.
I already see code like this appearing everywhere.
a, b := try( f() )
if a != 0 && b != "" {
...
}
...
in stead of
a,b,err := f()
if err != nil {
...
}
...
or
a,b,_:= f()
The call if err....
patern was a little unnatural at the begining for me but now i'm used to
I feel easier to deal with errors as they may arrive in the execution flow in stead of writing wrappers/handlers that will have to keep track of some kind of state to act once fired.
And if i decide to ignore errors to save my keyboard's life, i'm aware i'll panic one day.
i even changed my habits in vbscript to :
on error resume next
a = f()
if er.number <> 0 then
...
end if
...
I like this proposal
All of the concerns I had (e.g. ideally it should be a keyword and not a built in) are addressed by the in-depth document
It is not 100% perfect, but it is a good enough solution that a) solves an actual problem and b) does so while considering a lot of backwards compat and other issues
Sure it does some 'magic' but then so does defer
. The only difference is keyword vs. builtin, and the choice to avoid a keyword here makes sense.
I feel like all the important feedback against the try()
proposal was voiced already. But let me try to summarize:
1) try() moves vertical code complexity to horizontal
2) Nested try() calls are as hard to read as ternary operators
3) Introduces invisible 'return' control flow that's not visually distinctive (compared to indented blocks starting with return
keyword)
4) Makes error wrapping practice worse (context of function instead of a specific action)
5) Splits #golang community & code style (anti-gofmt)
6) Will make devs rewrite try() to if-err-nil and vice versa often (tryhard vs. adding cleanup logic / additional logs / better error context)
@VojtechVitek I think the points you make are subjective and can only be evaluated once people start to use it seriously.
However I believe there is one technical point that has not been discussed much. The pattern of using defer
for error wrapping/decoration has performance implications beyond the simple cost of defer
itself since functions that use defer
cannot be inlined.
This means that adopting try
with error wrapping imposes two potential costs compared with returning a wrapped error directly after an err != nil
check:
Even though there are some impressive upcoming performance improvements for defer
the cost is still non-zero.
try
has a lot of potential so it would be good if the Go team could revisit the design to allow some kind of wrapping to be done at the point of failure instead of pre-emptively via defer
.
vet" story needs a much more concrete plan
vet story is fairy tale. It will only work for known types and will be useless on custom ones.
Hi everyone,
Our goal with proposals like this one is to have a community-wide discussion about implications, tradeoffs, and how to proceed, and then use that discussion to help decide on the path forward.
Based on the overwhelming community response and extensive discussion here, we are marking this proposal declined ahead of schedule.
As far as technical feedback, this discussion has helpfully identified some important considerations we missed, most notably the implications for adding debugging prints and analyzing code coverage.
More importantly, we have heard clearly the many people who argued that this proposal was not targeting a worthwhile problem. We still believe that error handling in Go is not perfect and can be meaningfully improved, but it is clear that we as a community need to talk more about what specific aspects of error handling are problems that we should address.
As far as discussing the problem to be solved, we tried to lay out our vision of the problem last August in the “Go 2 error handling problem overview,” but in retrospect we did not draw enough attention to that part and did not encourage enough discussion about whether the specific problem was the right one. The try
proposal may be a fine solution to the problem outlined there, but for many of you it’s simply not a problem to solve. In the future we need to do a better job drawing attention to these early problem statements and making sure that there is widespread agreement about the problem that needs solving.
(It is also possible that the error handling problem statement was entirely upstaged by publishing a generics design draft on the same day.)
On the broader topic of what to improve about Go error handling, we would be very happy to see experience reports about what aspects of error handling in Go are most problematic for you in your own codebases and work environments and how much impact a good solution would have in your own development. If you do write such a report, please post a link on the Go2ErrorHandlingFeedback page.
Thank you to everyone who participated in this discussion, here and elsewhere. As Russ Cox has pointed out before, community-wide discussions like this one are open source at its best. We really appreciate everyone’s help examining this specific proposal and more generally in discussing the best ways to improve the state of error handling in Go.
Robert Griesemer, for the Proposal Review Committee.
Thank you, Go Team, for the work that went into the try proposal. And thanks to the commenters who struggled with it and proposed alternatives. Sometimes these things take on a life of their own. Thank you Go Team for listening and responding appropriately.
Yay!
Thanks everyone for hashing this out so we could have the best possible outcome!
The call is for a list of problems and negative experiences with Go's error handling. However,
Myself and Teams are very happy with xerrors.As, xerrors.Is and xerrors.Errorf in production. These new additions completely change error handling in a wonderful way for us now that we have fully embraced the changes. At the moment we've not encountered any issues or needs that are not addressed.
@griesemer Just wanted to say thank you (and probably many others who worked with you) for your patience and efforts.
good!
@griesemer Thank you and everyone else on the Go team for tirelessly listening to all the feedback and putting up with all our varied opinions.
So maybe now is a good time to bring this thread to closure and to move on to future things?
@griesemer @rsc and @all , cool, thanks all. to me, it's a great discussion/ identify / clarify. the enhancement of some part like 'error' issue in go , need more open discussion ( in proposal and comments ...) to identify / clarify the core issues first.
ps, the x/xerrors is good for now.
(might make sense to lock this thread as well...)
Thanks to the team and community for engaging on this. I love how many people care about Go.
I really hope the community sees first the effort and skill that went into the try proposal in the first place, and then the spirit of the engagement that followed that helped us reach this decision. The future of Go is very bright if we can keep this up, especially if we can all maintain positive attitudes.
func M() (Data, error){
a, err1 := A()
b, err2 := B()
return b, nil
} => (if err1 != nil){ return a, err1}.
(if err2 != nil){ return b, err2}
Okay... I liked this proposal but I love the way the community and Go team reacted and engaged in a constructive discussion, even though it was sometimes a bit rough.
I have 2 questions though regarding this outcome:
1/ Is "error handling" still an area of research?
2/ Do defer improvements get reprioritized?
This proves once again that the Go community is being heard and able to discuss controversial language change proposals. Like the changes that make it into the language, the changes that don't are an improvement. Thank you, Go team and community, for the hard work and civilized discussion around this proposal!
Excellent!
awesome,quite helpful
Maybe I'm too attached to Go, but I think a point was shown here, as
Russ described: there's a point where the community is not just a
headless chicken, it is a force to be reckoned with and to be
harnessed for its own good.
With due thanks to the coordination provided by the Go Team, we can
all be proud that we arrived to a conclusion, one we can live with and
will revisit, no doubt, when the conditions are more ripe.
Let's hope the pain felt here will serve us well in the future
(wouldn't it be sad, otherwise?).
Lucio.
I don’t agree on the decision. However I absolutely endorse approach the go team has undertaken. Having a community wide discussion and considering feedback from developers is what open source meant to be.
I wonder about whether the defer-optimizations will come too. I like annotating errors with it and xerrors together quite a lot and it's too costly right now.
@pierrec I think we need a clearer understanding of what changes in error handling would be useful. Some of the error values changes will be in the upcoming 1.13 release (https://tip.golang.org/doc/go1.13#errors), and we will gain experience with them. In the course of this discussion we have seen many many many syntactical error handling proposals, and it would be helpful if people could vote and comment on any that seem particularly useful. More generally, as @griesemer said, experience reports would be helpful.
It would also be useful to better understand to what extent error handling syntax is problematic for people new to the language, though that will be hard to determine.
There is active work on improving defer
performance in https://golang.org/cl/183677, and unless some major obstacle is encountered I would expect that to make it into the 1.14 release.
@griesemer @ianlancetaylor @rsc Do you still plan to address error handling verbosity, with another proposal solving some or all of the issues raised here?
So, late to the party, since this has already been declined, but for future discussion on the topic, what about a ternary-like conditional return syntax? (I didn't see anything similar to this in my scan of the topic or looking over the view of it Russ Cox posted on Twitter.) Example:
f, err := Foo()
return err != nil ? nil, err
Returns nil, err
if err is non-nil, continues execution if err is nil. The statement form would be
return <boolean expression> ? <return values>
and this would be syntactical sugar for:
if <boolean expression> {
return <return values>
}
The primary benefits is that this is more flexible than a check
keyword or try
built-in function, because it can trigger on more than errors (ex. return err != nil || f == nil ? nil, fmt.Errorf("failed to get Foo")
, on more than just the error being non-nil (ex. return err != nil && err != io.EOF ? nil, err
), etc, while still being fairly intuitive to understand when read (especially for those used to reading ternary operators in other languages).
It also ensures that the error handling _still takes place at call location_, rather than automagically happening based on some defer statement. One of the biggest gripes I had with the original proposal is that it attempts to, in some ways, make the actual _handling_ of errors an implicit processes that just happens automagically when the error is non-nil, with no clear indication that the control flow will return if the function call returns a non-nil error. The entire _point_ of Go using explicit error returns instead of an exception-like system is to encourage developers to explicitly and intentionally check and handle their errors, rather than just letting them propagate up the stack to be, in theory, handled at some point higher up. At least an explicit, if conditional, return statement clearly annotates what's going on.
@ngrilly As @griesemer said, I think we need to better understand what aspects of error handling Go programmers find most problematic.
Speaking personally, I don't think a proposal that removes a small amount of verbosity is worth doing. After all, the language works well enough today. Every change carries a cost. If we are going to make a change, we need a significant benefit. I think this proposal did provide a significant benefit in reduced verbosity, but clearly there is a significant segment of Go programmers who feel that the additional costs it imposed were too high. I don't know whether there is a middle ground here. And I don't know whether the problem is worth addressing at all.
@kaedys This closed and extremely verbose issue is definitely not the right place to discuss specific alternative syntaxes for error handling.
@ianlancetaylor
I think this proposal did provide a significant benefit in reduced verbosity, but clearly there is a significant segment of Go programmers who feel that the additional costs it imposed were too high.
I'm afraid there is a self-selection bias. Go is known for its verbose error handling, and its lack of generics. This naturally attracts developers that don't care about these two issues. In the meantime, other developers keep using their current languages (Java, C++, C#, Python, Ruby, etc.) and/or switch to more modern languages (Rust, TypeScript, Kotlin, Swift, Elixir, etc.) because of this. I know many developers who avoid Go mostly for this reason.
I also think there is a confirmation bias at play. Gophers have been used to defend the verbose error handling and the lack of error handling when people criticize Go. This makes harder to objectively assess a proposal like try.
Steve Klabnik published an interesting comment on Reddit a few days ago. He was against introducing ?
in Rust, because it was "two ways to write the same thing" and it was "too implicit". But now, after having written more than a few lines of code with, ?
is one of his favorite features.
@ngrilly I agree with your comments. Those biases are very difficult to avoid. What would be very helpful is a clearer understanding of how many people avoid Go due to the verbose error handling. I'm sure the number is non-zero, but it's difficult to measure.
That said, it's also true that try
introduced a new change in flow of control that was hard to see, and that although try
was intended to help with handling errors it did not help with annotating errors.
Thanks for the quote from Steve Klabnik. While I appreciate and agree with the sentiment, it is worth considering that as a language Rust seems somewhat more willing to rely on syntactic details than Go has been.
As a supporter of this proposal I'm naturally disappointed that it's now been withdrawn though I think the Go team has done the right thing in the circumstances.
One thing that now seems quite clear is that the majority of Go users don't regard the verbosity of error handling as a problem and I think that's something the rest of us will just have to live with even if it does put off potential new users.
I've lost count of how many alternative proposals I've read and, whilst some are quite good, I haven't seen any that I thought were worth adopting if try
were to bite the dust. So the chance of some middle ground proposal now emerging seems remote to me.
On a more positive note, the current discussion has pointed out ways in which all potential errors in a function can be decorated in the same manner and in the same place (using defer
or even goto
) which I hadn't previously considered and I do hope the Go team will at least consider changing go fmt
to allow single statement if
's to be written on one line which will at least make error handling _look_ more compact even if it doesn't actually remove any boilerplate.
@pierrec
1/ Is "error handling" still an area of research?
It has been, for over 50 years! There does not seem to be an overall theory or even a practical guide for consistent and systematic error handling. In the Go land (as for other languages) there is even confusion about what an error is. For example, an EOF may be an exceptional condition when you try to read a file but why it is an error? Whether that is an actual error or not really depends on the context. And there are other such issues.
Perhaps a higher level discussion is needed (not here, though).
Thank you @griesemer @rsc and everyone else involved with proposing. Many others have said it above, and it bears repeating that your efforts in thinking through the problem, writing the proposal, and discussing it in good faith, are appreciated. Thank you.
@ianlancetaylor
Thanks for the quote from Steve Klabnik. While I appreciate and agree with the sentiment, it is worth considering that as a language Rust seems somewhat more willing to rely on syntactic details than Go has been.
I agree in general about Rust relying more than Go on syntactic details, but I don't think this applies to this specific discussion about error handling verbosity.
Errors are values in Rust like they are in Go. You can handle them using standard control flow, like in Go. In the first versions of Rust, it was the only way to handle errors, like in Go. Then they introduced the try!
macro, which is surprisingly similar to the try
built-in function proposal. They eventually added the ?
operator, which is a syntactic variation and a generalization of the try!
macro, but this is not necessary to demonstrate the usefulness of try
, and the fact that the Rust community doesn't regret having added it.
I'm well aware of the massive differences between Go and Rust, but on the topic of error handling verbosity, I think their experience is transposable to Go. The RFCs and discussions related to try!
and ?
are really worth reading. I've been really surprised by how similar are the issues and arguments for and against the language changes.
@griesemer, you announced the decision to decline the try
proposal, in its current form, but you didn't say what the Go team is planning to do next.
Do you still plan to address error handling verbosity, with another proposal that would solve the issues raised in this discussion (debugging prints, code coverage, better error decoration, etc.)?
I agree in general about Rust relying more than Go on syntactic details, but I don't think this applies to this specific discussion about error handling verbosity.
Since others are still adding their two cents, I guess there is still room for me to do the same.
Though I have been programming since 1987, I have only been working with Go for about a year. Back about 18 months ago when I was looking for a new language to meet certain needs I looked at both Go and Rust. I decided on Go because I felt Go code was much easier to learn and use, and that Go code was far more readable because Go seems to prefer words to convey meaning instead of terse symbols.
So I for one would be very unhappy to see Go become more Rust-like, including the use of exclamation points (!
) and question marks (?
) to imply meaning.
In a similar vein, I think the introduction of macros would change the nature of Go and would result in thousands of dialects of Go as is effectively the case with Ruby. So I hope macros never get added Go, either, although my guess is there is little chance of that happening, fortunately IMO.
@ianlancetaylor
What would be very helpful is a clearer understanding of how many people avoid Go due to the verbose error handling. I'm sure the number is non-zero, but it's difficult to measure.
It's not a "measure" per se, but this Hacker News discussion provides tens of comments from developers unhappy with Go error handling due to its verbosity (and some comments explain their reasoning and give code examples): https://news.ycombinator.com/item?id=20454966.
First of all, thanks everybody for the supportive feedback on the final decision, even if that decision was not satisfactory for many. This was truly a team effort, and I'm really happy that we all managed to get through the intense discussions in an overall civil and respectful way.
@ngrilly Speaking just for myself, I still think it would be nice to address error handling verbosity at some point. That said, we have just dedicated quite a bit of time and energy on this over the last half year and especially the last 3 months, and we were quite happy with the proposal, yet we have obviously underestimated the possible reaction towards it. Now it does make a lot of sense to step back, digest and distill the feedback, and then decide on the best next steps.
Also, realistically, since we don't have unlimited resources, I see thinking about language support for error handling go on the back-burner for a bit in favor of more progress on other fronts, most notably work on generics, at least for the next few months. if err != nil
may be annoying, but it's not a reason for urgent action.
If you like to continue the discussion, I would like to gently suggest to everybody to move off from here and continue the discussion elsewhere, in a separate issue (if there's a clear proposal), or in other forums better suited for an open discussion. This issue is closed, after all. Thanks.
I'm afraid there is a self-selection bias.
I'd like to coin a new term here and now: "creator bias". If someone is willing to put the work, they should be given the benefit of the doubt.
It's very easy for the peanut gallery to shout loud and wide on unrelated forums how they dislike a proposed solution to a problem. It's also very easy for everyone to write a 3-paragraph incomplete attempt for a different solution (with no real work presented on the sideline). If one agrees with the status quo, ok. Fair point. Presenting anything else as a solution without a complete prposal gives you -10k points.
I don't support or am against try, but I trust Go Teams' judgement on the matter, so far their judgement has provided with an excellent language, so I think whatever they decide will work for me, try or no try, I consider we need to understand as outsiders, that the maintainers have broader visibility over the matter. syntax we can discuss all day. I'd like to thank everyone who has worked on or is trying to improve go at the moment for their efforts, we are thankful and look forward for new (non-backwards-breaking) improvements in the language libraries and Runtime if any is deemed useful by you guys.
It's also very easy for everyone to write a 3-paragraph incomplete attempt for a different solution (with no real work presented on the sideline).
The only thing I (and a number of others) wanted to make try
useful was an optional argument to allow it to return a wrapped version of the error instead of the unchanged error. I don't think that needed a huge amount of design work.
Oh no.
I see. Go want to make something different from other languages.
Maybe someone should lock this issue? The discussion is probably better suited elsewhere.
This issue is already so long that locking it seems pointless.
Everyone, please be aware that this issue is closed, and the comments you make here will almost certainly be ignored forever. If that is OK with you, comment away.
In case someone hate the try word which let them think of the Java, C* language, I advice not to use 'try' but other words like 'help' or 'must' or 'checkError'.. (ignore me)
In case someone hate the try word which let them think of the Java, C* language, I advice not to use 'try' but other words like 'help' or 'must' or 'checkError'.. (ignore me)
There will always be overlapping keywords and concepts which have small or large semantic differences in languages which are reasonably near each other (like C-family languages). A language feature should not cause confusion inside the language itself, differences between languages will always happen.
bad. this is anti pattern, disrespect author of that proposal
@alersenkevich Please be polite. Please see https://golang.org/conduct.
I think I'm glad about the decision to not go further with this. To me this felt like a quick hack to solve a small issue regarding if err != nil being on multiple lines. We don't want to bloat Go with minor keywords to solve minor things like this do we? This is why the proposal with hygienic macros https://github.com/golang/go/issues/32620 feels better. It tries to be a more generic solution to open up more flexibility with more things. Syntax and usage discussion ongoing there, so don't just think if it being C/C++ macros. The point there is to discuss a better way to do macros. With it, you could implement your own try.
I would love feedback on a similar proposal that addresses a problem with current error handling https://github.com/golang/go/issues/33161.
Honestly this should be reopened, out of all the err handling proposals, this is the most sane one.
@OneOfOne respectfully, I disagree that this should be reopened. This thread has established that there are real limitations with the syntax. Perhaps you are right that this is the most "sane" proposal: but I believe that the status quo is more sane still.
I agree that if err != nil
is written far too often in Go- but having a singular way to return from a function hugely improves readability. While I can generally get behind proposals that reduce boilerplate code, the cost should never be readability IMHO.
I know a lot of developers lament the "longhand" error checking in go, but honestly terseness is often at odds with readability. Go has many established patterns here and elsewhere that encourage a particular way of doing things, and, in my experience, the result is reliable code that ages well. This is critical: real-world code has to be read and understood many times throughout its lifetime, but is only ever written once. Cognitive overhead is a real cost, even for experienced developers.
Instead of:
f := try(os.Open(filename))
I'd expect:
f := try os.Open(filename)
Everyone, please be aware that this issue is closed, and the comments you make here will almost certainly be ignored forever. If that is OK with you, comment away.
—@ianlancetaylor
It would be nice If we could use try for a block of codes alongside with the current way of handling errors.
Something like this:
// Generic Error Handler
handler := func(err error) error {
return fmt.Errorf("We encounter an error: %v", err)
}
a := "not Integer"
b := "not Integer"
try(handler){
f := os.Open(filename)
x := strconv.Atoi(a)
y, err := strconv.Atoi(b) // <------ If you want a specific error handler
if err != nil {
panic("We cannot covert b to int")
}
}
The code above seems cleaner than the initial comment. I wish I could purpose this.
I made a new proposal #35179
val := try f() (err){
panic(err)
}
I hope so:
i, err := strconv.Atoi("1")
if err {
println("ERROR")
} else {
println(i)
}
or
i, err := strconv.Atoi("1")
if err {
io.EOF:
println("EOF")
io.ErrShortWrite:
println("ErrShortWrite")
} else {
println(i)
}
@myroid I wouldn't mind having your second example made a little bit more generic in a form of switch-else
statement:
```go
i, err := strconv.Atoi("1")
switch err != nil; err {
case io.EOF:
println("EOF")
case io.ErrShortWrite:
println("ErrShortWrite")
} else {
println(i)
}
@piotrkowalczuk Your code looks much better than mine. I think the code can be more concise.
i, err := strconv.Atoi("1")
switch err {
case io.EOF:
println("EOF")
case io.ErrShortWrite:
println("ErrShortWrite")
} else {
println(i)
}
This does not consider the option there will be an eye of different type
There needs to be
Case err!=nil
For errors the developer did not explicitly capture
On Fri, 8 Nov 2019, 12:06 Yang Fan, notifications@github.com wrote:
@piotrkowalczuk https://github.com/piotrkowalczuk Your code looks much
better than mine. I think the code can be more concise.i, err := strconv.Atoi("1")switch err {case io.EOF:
println("EOF")case io.ErrShortWrite:
println("ErrShortWrite")
} else {
println(i)
}—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4VH4KS2OX4M5BVH673QSU24DA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEDPTTYY#issuecomment-551500259,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ABNEY4W4XIIHGUGIW2KXRPTQSU24DANCNFSM4HTGCZ7Q
.
switch
doesn't need an else
, it has default
.
I've opened https://github.com/golang/go/issues/39890 which proposes something similar to Swift's guard
should address some of the concerns with this proposal:
It has not gained much traction but might be of interest for those who commented here.
Most helpful comment
Hi everyone,
Our goal with proposals like this one is to have a community-wide discussion about implications, tradeoffs, and how to proceed, and then use that discussion to help decide on the path forward.
Based on the overwhelming community response and extensive discussion here, we are marking this proposal declined ahead of schedule.
As far as technical feedback, this discussion has helpfully identified some important considerations we missed, most notably the implications for adding debugging prints and analyzing code coverage.
More importantly, we have heard clearly the many people who argued that this proposal was not targeting a worthwhile problem. We still believe that error handling in Go is not perfect and can be meaningfully improved, but it is clear that we as a community need to talk more about what specific aspects of error handling are problems that we should address.
As far as discussing the problem to be solved, we tried to lay out our vision of the problem last August in the “Go 2 error handling problem overview,” but in retrospect we did not draw enough attention to that part and did not encourage enough discussion about whether the specific problem was the right one. The
try
proposal may be a fine solution to the problem outlined there, but for many of you it’s simply not a problem to solve. In the future we need to do a better job drawing attention to these early problem statements and making sure that there is widespread agreement about the problem that needs solving.(It is also possible that the error handling problem statement was entirely upstaged by publishing a generics design draft on the same day.)
On the broader topic of what to improve about Go error handling, we would be very happy to see experience reports about what aspects of error handling in Go are most problematic for you in your own codebases and work environments and how much impact a good solution would have in your own development. If you do write such a report, please post a link on the Go2ErrorHandlingFeedback page.
Thank you to everyone who participated in this discussion, here and elsewhere. As Russ Cox has pointed out before, community-wide discussions like this one are open source at its best. We really appreciate everyone’s help examining this specific proposal and more generally in discussing the best ways to improve the state of error handling in Go.
Robert Griesemer, for the Proposal Review Committee.