go: Proposal: A built-in Go error check function, "try"
Proposal: A built-in Go error check function, 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.
Credits
This proposal is strongly influenced by the feedback we have received so far. Specifically, it borrows ideas from:
- Key Parts of Error Handling,
- issue #31442
- and, related, issue #32219.
Detailed design doc
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md
tryhard tool for exploring impact of try
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 1440
- Comments: 812 (331 by maintainers)
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
tryproposal 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.
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:rather than:
but now we would have:
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 thistryproposal;?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 proposedtry()already does.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: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.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 bygofmtstandards.returnis 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 singlereturn, so it looks like the only thing that the function ever should return isnil. The last twotrycalls are easy to see, but the first two are a bit harder, and would be even harder if they were nested somewhere, ie something likeproc := 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.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.
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 intry(). That feels unfortunate.I think these concerns would be addressed with a tweak:
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():errors.Wrap(err, "context message")a, b, err := myFunc()linedefer fmt.HandleError(&err, "msg")is still possible, but doesn’t need to be encouraged.checkis slightly simpler, because it doesn’t need to return an arbitrary number of arguments from the function it is wrapping.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}
panic(...)is a relatively clear exception (pun not intended) to the rule thatreturnis the only way out of a function. I don’t think we should use its existence as justification to add a third.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
deferstatements 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.
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
errorasexception, 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
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 messagereturn 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 withtry. So if the argument fortryis that it will eliminate allif err != nilblocks, 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 != nilis just part of Go, like the curly braces, ordefer. 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 © 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.
Hacker news has some point:
trydoesn’t behave like a normal function (it can return) so it’s not good to give it function-like syntax. Areturnordefersyntax would be more appropriate:Instead of:
I’d expect:
I see two problems with this:
It puts a LOT of code nested inside functions. That adds a lot of extra cognitive load, trying to parse the code in your head.
It gives us places where the code can exit from the middle of a statement.
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:
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 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 fmtshould let it sit on one line).The
try()concept has all the problems ofcheckfrom 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
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)andf(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
errorand 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
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:
In general, using a built-in for the
tryfunctionality 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”);tryas an expression seems not a good idea, it harms readability (@ChrisHines, @jimmyfrasche), they are “returns without areturn”. @brynbellomy did an actual analysis oftryused 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.@crawshaw took some time to analyze a couple hundred use cases from the std library and came to the conclusion that
tryas proposed almost always improved readability. @jimmyfrasche came to the opposite conclusion.Another theme is that using
deferfor error decoration is not ideal. @josharian points out thedefer’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.Many wrote up suggestions for improving the proposal. @zeebo, @patrick-nyt are in supoort of
gofmtformatting simpleifstatements on a single line (and be happy with the status quo). @jargv suggested thattry()(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 adefer; @masterada suggested usingerrorfunc()instead. @velovix revived the idea of a 2-argumenttrywhere the 2nd argument would be an error handler.@klaidliadon, @networkimprov are in favor of special “assignment operators” such as in
f, # := os.Open()instead oftry. @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-specifictry(see issue #32473). @josharian revived a possibility we discussed at GopherCon last year wheretrydoesn’t return upon an error but instead jumps (with agoto) to a label namederror(alternatively,trymight take the name of a target label).tryas a keyword, two lines of thoughts have appeared. @brynbellomy suggested a version that might alternatively specify a handler:@thepudds goes a step further and suggests
tryat the beginning of the line, givingtrythe same visibility as areturn:Both of these could work with
defer.I was somewhat concerned about the readability of programs where
tryappears inside other expressions. So I rangrep "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
tryapplies, 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
tryinside another expression. The typical case is statements of the formx := try(...)or^try(...)$.Here are a few examples where
trywould appear inside another expression:text/template
becomes:
text/template
becomes
(this is the most questionable example I saw)
regexp/syntax:
becomes
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
candtare living beyond the scope of the if statement.net/http
net/http/request.go:readRequest
becomes:
database/sql
becomes
database/sql
becomes
net/http
becomes
net/http
becomes
(This one I really like.)
net/http
becomes
}
(Also nice.)
net:
becomes
maybe this is too much, and instead it should be:
Overall, I quite enjoy the effect of
tryon the standard library code I read through.One final point: Seeing
tryapplied 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 usetry(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.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
trygiven enough getting used, but I also do feel we will need extra IDE support (or some such) to highlighttryto efficiently recognize the implicit flow in code reviews/debugging sessionsFirst 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 != nilmay 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.
@ianlancetaylor
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.@griesemer Just wanted to say thank you (and probably many others who worked with you) for your patience and efforts.
Luckily,
gois 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!
One of the examples in the proposal nails the problem for me:
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
trywill automatically imply that eventuallycatchwill 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.
我只是一个普通的 go 开发者,并且我也没有做很多底层的开发,主要用 go 来做web开发。从使用感觉上来讲,go 的错误处理机制真的很难用。真的很难理解为什么不用try catch?实现一套异常错误处理机制,就像大部分的语言一样。
这样用不是更好吗?(我自己使用 recover 机制实现了一个try catch)
一点猜想
try catch 大概率不会被官方支持了,golang 的错误处理机制,积重难返。如果使用 try catch 这样的机制,golang的官方包,三方包统统都要改造~
It’s an absolutely bad idea.
errvar appear fordefer? What about “explicit better than implicit”?deferwill create a lot of ugly and hard-to-understand code.os.Exit, your errors will be unchecked.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.
On the positive side, @rasky, @adg, @eandre, @dpinela, and others explicitly expressed happiness over the code simplification that
tryprovides.The most important concern appears to be that
trydoes 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.)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).trymay also be easily overlooked (@peterbourgon), especially because it can appear in expressions that may be arbitrarily nested. @natefinch is concerned thattrymakes it “too easy to dump too much in one line”, something that we usually try to avoid in Go. Also, IDE support to emphasizetrymay not be sufficient (@dominikh);tryneeds to “stand on its own”.For some, the status quo of explicit
ifstatements 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 explicitifstatements are better than implicitreturn’s (@DavexPro, @hmage, @prologic, @natefinch). Along the same lines, @mattn is concerned about the “implicit binding” of the error result totry- the connection is not explicitly visible in the code.Using
trywill make it harder to debug code; for instance, it may be necessary to rewrite atryexpression back into anifstatement just so that debugging statements can be inserted (@deanveloper, @typeless, @networkimprov, others).There’s some concern about the use of named returns (@buchanae, @adg).
Several people have provided suggestions to improve or modify the proposal:
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.@pierrec suggested that
gofmtcould formattryexpressions suitably to make them more visible. Alternatively, one could make existing code more compact by allowinggofmtto formatifstatements checking for errors on one line (@zeebo).@marwan-at-work argues that
trysimply shifts error handling fromifstatements totryexpressions. 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).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):
Thanks for the positive feedback! 😃
It would be good to learn more about this concern. The current coding style using
ifstatements 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 eachif). Often it makes sense to handle all errors detected in a function in a uniform way, which can be done with adefer- 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 todefer, that led us to leave away a new mechanism solely for augmenting errors.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.
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.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 atryexpression into anifstatement could be annoying.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.
Optional handler argument to
try: The detailed document discusses this as well. See the section on Design iterations.Using
gofmtto formattryexpressions such that they are extra visible would certainly be an option. But it would take away from some of the benefits oftrywhen used in an expression.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 typeerror, 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.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.
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”.
bad. this is anti pattern, disrespect author of that proposal
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.
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 “
panicalso controls the flow of the program!”. However, there are a few reasons that it’s more okay forpanicto do this that don’t apply totry.panicis friendly to beginner programmers because what it does is intuitive, it continues unwrapping the stack. One shouldn’t even have to look up howpanicworks in order to understand what it does. Beginner programmers don’t even need to worry aboutrecover, 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.panicis a name that is easy to see. It brings worry, and it needs to. If one seespanicin a codebase, they should be immediately thinking of how to avoid the panic, even if it’s trivial.Piggybacking off of the last point,
paniccannot 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
tryfunction satisfies none of these points.One cannot guess what
trydoes 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.trydoes 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, wheretryis seen as unnecessary boilerplate (because of checked exceptions).trycan be used in an argument to a function call, as per my example in my previous commentproc := try(os.FindProcess(try(strconv.Atoi(os.Args[1])))). This makes it even harder to spot.My eyes ignore the
tryfunctions, even when I am specifically looking for them. My eyes will see them, but immediately skip to theos.FindProcessorstrconv.Atoicalls.tryis 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.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.
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
tryrarely ends up inside another expression, thus directly refuting the concern thattrymight 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.)
@deanveloper It is true that this proposal (and for that matter, any proposal trying to attempt the same thing) will remove explicitly visible
returnstatements from the source code - that is the whole point of the proposal after all, isn’t it? To remove the boilerplate ofifstatements andreturnsthat are all the same. If you want to keep thereturn’s, don’t usetry.We are used to immediately recognize
returnstatements (andpanic’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 recognizetryas changing control flow after some getting used to it, just like we do forreturn. I have no doubt that good IDE support will help with this as well.Someone has already implemented this 5 years ago. If you are interested, you can try this feature
https://news.ycombinator.com/item?id=20101417
It would be helpful if this could be accompanied (at some stage of accepted-ness) by a tool to transform Go code to use
tryin some subset of error-returning functions where such a transformation can be easily performed without changing semantics. Three benefits occur to me:trycould be used in their codebase.trylands 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.trywill 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 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.
I appreciate the commitment to backwards compatibility that motivates you to make
trya builtin, rather than a keyword, but after wrestling with the utter weirdness of having a frequently-used function that can change control flow (panicandrecoverare extremely rare), I got to wondering: has anyone done any large-scale analysis of the frequency oftryas 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
trybeing 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
tryas a keyword.I’d propose the following constructions.
1) No handler
2) Handler
Note that error handlers are simple code blocks, intended to be inlined, rather than functions. More on this below.
Proposed restrictions:
trya function call. Notry err.tryfrom within a function that returns an error as its rightmost return value. There’s no change in howtrybehaves based on its context. It never panics (as discussed much earlier in the thread).Benefits:
try/elsesyntax could be trivially desugared into the existing “compound if”: becomes To my eye, compound ifs have always seemed more confusing than helpful for a very simple reason: conditionals generally occur after an operation, and have something to do with processing its results. If the operation is wedged inside of the conditional statement, it’s simply less obvious that it’s happening. The eye is distracted. Furthermore, the scope of the defined variables is not as immediately obvious as when they’re leftmost on a line.check/handlethat “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.returninside the handler to meansuper return. Hijacking a keyword is extremely confusing.returnjust meansreturn, and there’s no real need forsuper return.deferdoesn’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 != nilblocksgo vetcheck 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.
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.
@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
trya statement, I rewrote all of the examples using that syntax and found it much clearer than either the expression-tryor the status quo, without requiring too many extra lines:This one would arguably be better with an expression-
tryif there were multiple fields that had to betry-ed, but I still prefer the balance of this trade offThis is basically the worst-case for this and it looks fine:
I debated with myself whether
if trywould 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: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 totry()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.
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.
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.
tryas laid out in the proposal seems to be the least bad way of doing it.To clarify:
Does
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.
@gbbr You have a choice here. You could write it as:
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.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:
ifstatements areif <err> != niltrycandidatesLooking through the replacements that tryhard found, there are certainly types of code where the usage of
trywould 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):
Without
try, we just wroteunexpectedat the return points where it’s needed since there’s no great improvement by handling it in one place. However, withtry, we can apply theunexpectederror transformation with a defer and then dramatically shorten the code, making it clearer and easier to skim:Not sure why anyone ever would write a function like this but what would be the envisioned output for
try(foobar())If
foobarreturned(error, error)(might make sense to lock this thread as well…)
I am more than okay with this.
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() erroras well. It’d make writing little scripts much nicer. The semantics would be equivalent to:I feel like all the important feedback against the
try()proposal was voiced already. But let me try to summarize:returnkeyword)@ardan-bkennedy, sorry for the second followup but regarding:
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.
This proposal takes us from having
if err != nileverywhere, to havingtryeverywhere. 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 != nilis actually more readable thantrybecause it does not clutter the line of the business logic language, rather sits right below it: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
tryif a user does not assign an error. For example: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
iferrif 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.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:https://github.com/purpleidea/mgmt/blob/a235b760dc3047a0d66bb0b9d63c25bc746ed274/util/errwrap/errwrap.go#L26
I really think this absolutely should not be a built-in function.
It has been mentioned a few time that
panic()andrecover()also alter the control flow. Very well, let us not add more.@networkimprov wrote https://github.com/golang/go/issues/32437#issuecomment-498960081:
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 bego(func())?Since the debate here seems to continue unabated, let me repeat again:
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 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. Nowappend()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. Thetry()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 withiflike we always have, because at least, this is consistent and had the benefit of “there is only one way to do it”.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.Just a quick comment backed with data of a small sample set:
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?
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.
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.
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”.
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).
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.
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’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.
@davecheney @daved @crawshaw I’d tend to agree with the Daves on this one: in @crawshaw’s examples, there are lots of
trystatements embedded deep in lines that have a lot of other stuff going on. Really hard to spot exit points. Further, thetryparens 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:
tryas a keywordtryper linetrymust 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.
tryremains very visible anywhere it’s used.text/template
becomes:
text/template
becomes
regexp/syntax:
becomes
net/http
net/http/request.go:readRequest
becomes:
database/sql
becomes
database/sql
becomes
net/http
becomes
net/http This one doesn’t actually save us any lines, but I find it much clearer because
if err == nilis a relatively uncommon construction.becomes
net/http
becomes
net:
becomes
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.
The other problem I have with
tryis 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.^^ 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.
^^ 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.
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:
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 -
According to the proposal:
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’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 != nilprovides a very easy to read, albeit vertically verbose, idiom. The spirit of whattry()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 afterxerrand 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% ofifstatements are forif err != nilchecking). On the surface, it appears as though the reducing boilerplate associated with error handling will improve developer experiences. The tradeoff from the introduction oftry()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 ofif 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 withgo,defer, and named return variables makestry()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
scopelintmany 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 fromtry()is different than pinned variables, but there will be an impact on how people write code as a result.IMNSHO, the
xerrand generics proposals need time to bake in production for 6-12mo before attempting to conquer the boilerplate fromif 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 aroundtry()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 != nilinto the assumed behavior oftry(), the language is preventing the use oferras 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/casestatements 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
#nevertryresponse so much as it is, “not now, not yet, and let’s consider this again in the future afterxerrand generics mature in the ecosystem.”“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.
Re disabling/vetting try:
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.
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.Expression based flow control
panicmay be another flow controlling function, but it doesn’t return a value, making it effectively a statement. Compare this totry, which is an expression and can occur anywhere.recoverdoes have a value and affects flow control, but must occur in adeferstatement. Thesedefers are typically function literals,recoveris only ever called once, and sorecoveralso effectively occurs as a statement. Again, compare this totrywhich can occur anywhere.I think those points mean that
trymakes 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.Another proposal
Allow statements like
to be formatted on one line by
gofmtwhen the block only contains areturnstatement and that statement does not contain newlines. For example:Rationale
gofmtkeeps newlines if they already exist (like struct literals). Opt in also allows the writer to make some error handling be emphasizedgofmtreturnstatements, so it won’t be abused to golf code unnecessarilytryexpressions handles this poorlytryleans more towards the writertryexisting on multiple lines. For example this comment or this comment which introduces a style likeerrvalue 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 aredeferbased wrapping. Both raise the barrier to wrapping errors and the former may requiregodocchangestryversus using traditional error handlingtryor something else in the future. The change may be positive even iftryis acceptedtestinglibrary ormainfunctions. In fact, if the proposal allows any single lined statement instead of just returns, it may reduce usage of assertion based libraries. ConsiderIn 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
From https://github.com/golang/go/issues/32437#issuecomment-498941435
With try
With this
It’s competitive in space usage while still allowing for adding context to errors.
From https://github.com/golang/go/issues/32437#issuecomment-499007288
With try
With this
The original comment used a hypothetical
tryfto attach the formatting, which has been removed. It’s unclear the best way to add all the distinct contexts, and perhapstrywouldn’t even be applicable.I’m concerned that
trywill 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
tryeverything.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.
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 waytry(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 beatstry(). As such, I agree with @rasky .Edit: Well, I managed to completely forget that panic exists and isn’t a keyword.
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:
Returns
nil, errif err is non-nil, continues execution if err is nil. The statement form would beand this would be syntactical sugar for:
The primary benefits is that this is more flexible than a
checkkeyword ortrybuilt-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.
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!
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:
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.Here’s a modification that may help with some of the concerns raised: Treat
trylike agotoinstead of like areturn. Hear me out. 😃trywould instead be syntactic sugar for:Benefits:
deferis not required to decorate errors. (Named returns are still required, though.)error:label is a visual clue that there is atrysomewhere 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)wouldgoto wrapinstead ofgoto error. The compiler can confirm thatwrap: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:
Other comments:
trybe 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.trycould 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.
How about use only
?to unwrap result just likerust@dominikh The detailed proposal discusses this at length, but please note that
panicandrecoverare two built-ins that affect control flow as well.@sylr
Come on, that would be pretty acceptable.
@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
trywould indeed reduce boilerplate and simplify code; and there were a few comments - also based on data on real code - that showed thattrywould 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.
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”:
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:
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:
This proposal has revised that, but the idea is the same:
and we want to add an as-yet-unnamed helper for this common pattern:
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.
@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:
It just so happens that for error decoration we suggest to use a
deferand named result parameters (or ye oldeifstatement) 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, andtryis about as minimal a change we could come up with. And clearly, everything is still explicit in the code.@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).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.
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.
val := try f() (err){ panic(err) }
Honestly this should be reopened, out of all the err handling proposals, this is the most sane one.
@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.
Compact single-line if
As an addition to the single-line if proposal by @zeebo and others, the if statement could have a compact form that removes the
!= niland the curly braces:I think this is simple, lightweight & readable. There are two parts:
if variable return .... Since thereturnis 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:
One-line if:
One-line compact if:
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:There’s more to be done, but this gives a clearer picture. Specifically, 28% of all
ifstatements 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 totry.I’m in favour of this proposal. It avoids my largest reservation about the previous proposal: the non-orthogonality of
handlewith respect todefer.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,
tryis 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 usetryto avoid writing out all the return values each time. This is also potentially useful when returning by-value struct types with long names.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
trycan 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
tryas-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 howtryworks when assigning to struct fields. (That is assuming my reading of the proposal is correct, and that this works?)The existing code:
With
try:No loss of readability, except perhaps that it’s less obvious that
newScannermight fail. But then in a world withtryGo programmers would be more sensitive to its presence.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.Adding a few comments to my downvote.
For the specific proposal at hand:
I would greatly prefer this to be a keyword vs. a built-in function for previously articulated reasons of control flow and code readability.
Semantically, “try” is a lightning rod. And, unless there is an exception thrown, “try” would be better renamed to something like
guardorensure.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/ensureconcept vs. leavingif err != nilalone: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’.
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.
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:
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:
With try(), code is more readable, consistent and safer in my opinion:
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.
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:
Furthermore, in this proposal
tryis a function that returns values, so it may be used as part of a bigger expression.Some have argued that
panichas already set the precedent for a built in function that changes control flow, but I thinkpanicis fundamentally different for two reasons:Try on the other hand:
For these reasons I think
tryfeels 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
tryit 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
trythat 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 thattryadds 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.@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):I like
tryon separate line. And I hope that it can specifyhandlerfunc independently.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 withfmt.Errorfwrapping.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: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.
@dominikh
tryalways 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.@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.
tryis 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.
This will not work. The defer will update the local
errvariable, which is unrelated to the return value.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:
No one is going to make you use
try.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.
in stead of
or
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 :
@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
tryremoves 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.
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 fortryand 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/srcat tip reports > 5000 (!) opportunities fortry. 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
tryand lets us move past idle and unproductive speculation.Thanks & enjoy.
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:
but all things considered this is probably not a significant improvement over using existing language constructs:
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
tryis limited to the simple semanticsif 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.vs :
?
I like the idea of the comments listing
tryas 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 thedefer 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
tryas an expression is that it’s either too easy to gloss over, or not easy enough to gloss over. Take the following two examples:Try as an expression
Try as a statement
Takeaways
This code is definitely contrived, I’ll admit. But what I am getting at is that, in general,
tryas an expression doesn’t work well in:I however agree with @ianlancetaylor that beginning each line with
trydoes 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:
It reads a port from a
*bufio.Reader, starts a TCP connection, and copies a number of bytes specified by the same*bufio.Readertostdout. All with error handling. For a language with such strict coding conventions, I don’t think this should really even be allowed. I guessgofmtcould help with this, though.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:
To help with that, @brynbellomy suggested yesterday:
Taking that further, the
trycould be required to be the start of the line, even for an assignment.So it could be:
rather than the following (from @brynbellomy’s example):
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:
That could instead be:
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:
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
tryor 3try. In any event, perhaps it would be better to require spreading that across 2-3 lines that start withtry.@ChrisHines To your point (which is echoed elsewhere in this thread), let’s add another restriction:
trystatement (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
returnand conditional returns annotated bytry, 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:
but rather this:
which still feels clearer than this:
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: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.when a error is assigned to
#the function returns immediately with the error received. As for the other variables their values would be: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.
Concerns
Unfortunate overloading of defer
The preference to overload the obvious and straightforward nature of
deferas alarming. If I writedefer 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 usingdeferforpanic()andrecover()is less obvious, I rarely if ever use it and almost never see it when reading other’s code.Spoo to overload
deferto also handle errors is not obvious and confusing. Why the keyworddefer? Doesdefernot mean “Do later” instead of “Maybe to later?”Also there is the concern mentioned by the Go team about
deferperformance. Given that, it seems doubly unfortunate thatdeferis being considered for the “hot path” code flow.No stats verifying a significant use-case
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?
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: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™” 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?
Can (mostly) already implement
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: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 Gofuncto return a variadic number of return values, e.g.: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.)
Lack of obviousness
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 namederrinside 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 ownerrvariable in my function, to avoid clashes?And will it call all
defers? In reverse order or regular order?Or will it return from both the closure and the
funcwhere 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?”
Lack of control
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 trivialfuncit 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 apackage— but then also have more specific handling shared across one or more other packages.For example, I may call five (5)
funccalls that return anerror()from within anotherfunc; let’s label themA(),B(),C(),D(), andE(). I may needC()to have its own error handling,A(),B(),D(), andE()to share some error handling, andB()andE()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;
funcs and closures. So my rhetorical question is: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.
Lack of stated support for
breakThis 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
breakfor error handling wrapping most or all of a func prior toreturn.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 usedefer, which I find harder to reason about in terms of program flow.)To use
breakinstead of early return use afor range "1" {...}loop to create a block for the break to exit from (I actually create a package calledonlythat only contains a constant calledOncewith a value of"1"):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
returns and ignores usingbreakfor error handlingMy opinion
err == nilis 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
nilto 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
statusand three builtin functionsiserror(),iswarning(),issuccess().statuscould implementerror— allowing for much backward compatibility and anilvalue passed toissuccess()would returntrue— butstatuswould 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 anilcheck. That would allow something like the following approach instead: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.“Not for everyone” justification
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):
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?
Nesting of try()
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: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 liketry(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.In summary
At the time of this writing the
58%down votes to42%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.#fwiw
P.S. To put it more tongue-in-cheek, I think we should follow the paraphrased wisdom of Yoda:
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 != nilor give up on adding extra context, usetry()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 concernedtry()might encourage people to use common wrappers per function even when it’d make sense to wrap different errors differently.@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 usedtry()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:
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.
This looks like a special cased macro.
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/handleproposal progressing.Check/handle was a much more comprehensive approach to making error handling in go more concise. Its
handleblock retained the same function scope as the one it was defined in, whereas anydeferstatements 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 ashandle { 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.@piotrkowalczuk Your code looks much better than mine. I think the code can be more concise.
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.
Yay!
Here are some more raw
tryhardstats. This is only lightly validated, so feel free to point out errors. 😉First 20 “Popular Packages” on godoc.org
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
tryhardsettings, which in theory should be excludingvendordirectories.The median value for try candidates across these 20 repos is 58%.
The “if stmts” column is only tallying
ifstatements in functions returning an error, which is howtryhardreports it, and which hopefully explains why it is so low for something likegorm.10 misc. “large” Go projects
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%.
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.
@nvictor
Verbosity in error handling is not a problem, it is Go’s strength.
Yours opt-in at writing time is a must for all readers, including future-you.
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 (astryhardruns have shown).I’d argue that my way simpler onErr macro would spare more lines writing, and for the majority:
(note that I am in the ‘leave
if err!= nilalone’ camp and above counter proposal was published to show a simpler solution that can make more whiners happy.)Edit:
~Short to write, long to read, prone to slips or misunderstandings, flaky and dangerous at maintenance stage.~
I was wrong. Actually the variadic
trywould be much better than nests, as we might write it by lines:and have
try(…)return after the first err.I ran
tryhardon 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 ofif err != niltype 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 wheretrywould actually change the construct of the code.In other words, my take is that
tryin its current form gives me: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 $.02I just ran
tryhardon a package (with vendor) and it reported2478with the code count dropping from873934to851178but 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 likeerror: cannot process ....file: cannot parse ...file: cannot open ...file.<rant> I wouldn’t put too much weight on the number of votes unless you think that there’s only ~3000 Go developers out there. The high vote counts on the other non-proposal is simply due to the fact that the issue made it to the top of HN and Reddit — the Go community isn’t exactly known for its lack of dogma and/or nay-saying so no-one should be surprised about the vote counts.
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. </rant>
https://github.com/golang/go/issues/32437#issuecomment-502975437
@beoran Agreed. This is why I suggested that we unify the vast majority of error cases under the
trykeyword (tryandtry/else). Even though thetry/elsesyntax doesn’t give us any significant reduction in code length versus the existingif err != nilstyle, it gives us consistency with thetry(noelse) 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 oftrythat 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.
appendis the definitive way to add elements to a slice.makeis the definitive way to construct a new channel or map or slice (with the exception of literals, which I’m not thrilled about). Buttry()(as a builtin, and withoutelse) 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 oftrythat succeeds, consistency and readability will compel me not to use it, just as I try to avoid map/slice literals (and avoidnewlike 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.
@griesemer I’m indeed not that keen on
tryas a unary prefix operator for the reasons pointed out there. It has occurred to me that an alternative approach would be to allowtryas a pseudo-method on a function return tuple:That solves the precedence issue, I think, but it’s not really very Go-like.
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
tryas a statement (without all theelse { ... }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.
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
tryfunction. 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 overtryjust as you already glaze overif err != nil { return err }.@griesemer
+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:The language changes required would be:
Allow a
for error{}construct, similar tofor range{}but only entered upon an error and only executed once.Allow omitting the capturing of return values that implement
<object>.Error() stringbut only when afor error{}construct exists within the samefunc.Cause program control flow to jump to the first line of the
for error{}construct when anfuncreturns 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()What is an “error”?
Currently an “error” is defined as any object that implements
Error() stringand of course is notnil.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!=nilmeans “error” but instead check if an error object implements anIsError()and ifIsError()returnstruebefore assuming that any non-nilvalue 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!=nilwe 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:A side benefits of allowing the non-capturing of “errors”
Note that allowing the non-capturing of the last “error” from
funccalls would allow later refactoring to return errors fromfuncs 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 saidfuncs.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.
@ugorji Thanks for your positive feedback.
trycould be extended to take an additional argument. Our preference would be to take only a function with signaturefunc (error) error. If you want to panic, it’s easy to provide a one-line helper function:Better to keep the design of
trysimple.https://github.com/golang/go/issues/32437#issuecomment-498908380
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 sayingAnyway, Go’s pretty strict about simplicity:
gofmtmakes all the code look the same way. The happy path keeps left and anything that could be expensive or surprising is explicit.tryas is proposed is a 180 degree turn from this. Simplicity != concise.At the very least
tryshould be a keyword with lvalues.@MrTravisB
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
trydoesn’t really affect you. In fact, the rarer the use case is to you the less it affects you thattryis added.I don’t like the
tryname. 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 insteadtrywould be used in case we expect rare failures (motivation for wanting to reduce verbosity of error handling) and are optimistic. In additiontryin this proposal does in fact catches an error to return it early. I like thepasssuggestion 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.
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.
@ianlancetaylor
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 thetrybuilt-in function proposal. They eventually added the?operator, which is a syntactic variation and a generalization of thetry!macro, but this is not necessary to demonstrate the usefulness oftry, 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 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?
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.@ccbrown Got it. In retrospect, I think your suggested relaxation should be no problem. I believe we could relax
tryto work with any interface type (and matching result type), for that matter, not justerror, as long as the relevant test remainsx != nil. Something to think about. This could be done early, or retro-actively as it would be a backward-compatible change I believe.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
trycan 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!
@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.
please use try {} catch{} syntax, don’t build more wheels
Compiler flags should not change the spec of the language. This is much more fit for vet/lint
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.@ianlancetaylor Echoing @DmitriyMV, the
elseblock 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-handledtryblocks in real code):While the
try/elsepattern doesn’t save many characters over compoundif, it does:tryifs suffer fromUnhandled
trywill likely be the most common, though.@ianlancetaylor If I understand “try else” proposal correctly, it seems that
elseblock is optional, and reserved for user provided handling. In your exampletry a, b := f() else err { return nil, err }theelseclause is actually redundant, and the whole expression can be written simply astry a, b := f()I want to expand on @jimmyfrasche 's recent comment.
The goal of this proposal is to reduce the boilerplate
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
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
which cuts from 56 to 18 characters, a much more significant reduction. And while the
trystatement makes the potential change of flow of control more clear, overall I don’t find the statement more readable. Though on the plus side thetrystatement 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.
@thepudds, this is what I was getting at in my earlier comment. Except that given
An obvious thing to do is to think of
tryas a try block where more than one sentence can be put within parentheses . So the above can becomeIf the compiler knows how to deal with this, the same thing works for nesting as well. So now the above can become
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
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
While I am perfectly fine with
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.
In my opinion, using
tryto avoid writing out all the return values is actually just another strike against it.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
trydoesn’t mean “return”. And that’s how it’s being used here. I would actually prefer that the proposal change so thattrycan’t take anerrorvalue 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
At least that reads with some kind of logic.
But let’s not abuse
tryto solve some other problem.@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 theifless 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,
tryor notry, 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).
@zeebo It would be easy to make
gofmtformatif err != nil { return ...., err }on a single line. Presumably it would only be for this specific kind ofifpattern, not all “short”ifstatements?Along the same lines, there were concerns about
trybeing invisible because it’s on the same line as the business logic. We have all these options:Current style:
One-line
if:tryon a separate line (!):tryas proposed:The first and the last line seem the clearest (to me), especially once one is used to recognize
tryas 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.Being chaotic here, I’ll throw the idea of adding a second built-in function called
catchwhich will receive a function that takes an error and returns an overwritten error, then if a subsequentcatchis called it would overwrite the handler. for example:Now, this builtin function will also be a macro-like function that would handle the next error to be returned by
trylike this:This is nice because I can wrap errors without
deferwhich can be error prone unless we use named return values or wrap with another func, it is also nice becausedeferwould 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: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 😛Another thing occurs to me: I’m seeing a lot of criticism based on the idea that having
trymight 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:in order to be able to write
if exists(...) { ... }, even though this code silently ignores some possible errors. If I hadtry, I probably would not bother to do that and just return(bool, error).@MrTravisB
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 the Go source there are thousands of cases that could be handled by
tryout of the box even if there was no way to add context to errors. If minor, it’s still a common cause of complaint.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.
Again, then you don’t use
try. Then you gain nothing fromtry, but you also don’t lose anything.@MrTravisB
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.
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,
trydoes a thing, and if you don’t want that thing, don’t usetry.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 != nilAND 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:errat the beginning of the function. Does this work? I recall issues with defer & unnamed results. If it doesn’t the proposal needs to consider this.wrapffunction that has theif err != nilboilerplate.If either work, I can deal with it.
switchdoesn’t need anelse, it hasdefault.@myroid I wouldn’t mind having your second example made a little bit more generic in a form of
switch-elsestatement:@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 != nilis 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.
Thanks, @jonbodner, for your example. I’d write that code as follows (translation errors notwithstanding):
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. Thedeferin particular, together with the statusCode being changed on the way down and used in thedefermakes 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 withreturn statusCode, nilif you want) is simpler because it cleanly separates error handling from the “business logic”.This is an appreciation comment;
thanks @griesemer for the gardening and all, that you have been doing on this issue as well as elsewhere.
@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.
@iand
I asked this of @rsc a while ago (https://github.com/golang/go/issues/32437#issuecomment-503245958):
The answer was purposed, but uninspiring and lacking substance (https://github.com/golang/go/issues/32437#issuecomment-503295558):
Additional sentiment was offered (https://github.com/golang/go/issues/32437#issuecomment-503408184):
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.
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
trymacro 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:checkmacro is not a one-liner: it helps the most where many repetitive checks using the same expression should be performed in close proximity.Design constraints (met)
It is a built-in, it does not nest in a single line, it allows for way more flows than
tryand has no expectations about the shape of a code within. It does not encourage naked returns.usage example
Hope this helps, Enjoy!
I discussed this point already but it seems relevant - code complexity should scale vertically, not horizontally.
tryas an expression encourages code complexity to scale horizontally by encouraging nested calls.tryas a statement encourages code complexity to scale vertically.@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… 😃
A small thing, but if
tryis a keyword it could be recognized as a terminating statement so instead ofyou can just do
(
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)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 oftry x := thingie else err { handle(err) }don’t meet that bar. Why not just sayif?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.@jimmyfrasche
Your first example illustrates well why I strongly prefer the expression-
try. In your version, I have to put the result of the call tolein a variable, but that variable has no semantic meaning that the termledoesn’t already imply. So there’s no name I can give it that isn’t either meaningless (likex) or redundant (likelessOrEqual). 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.
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.
Also,
tryis 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.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.
I guess my main problem with
tryis that it’s really just apanicthat 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.After the complexities of the
check/handledraft 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
trydidn’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
deferis 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
trydoesn’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 (perhapsptryormust) which always panicked rather than returned on encountering a non-nil error and which could therefore be used with the aforementioned functions (includingmain). 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)ordefer 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 fmtdidn’t rewrite single lineifstatements. Personally, I would prefer a simple rule that this would be allowed for any single statementifwhether 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.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 andtrywill 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.As mentioned above,
trywill behave in accordance with the original proposal. There would be no such thing as an absent error handler, only a nil one.I believe that the enclosing function would return with a nil error. It would potentially be very confusing if
trycould 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.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
trys because they are less brief. Some might see this as a downside, but I think it’s a benefit.Sorry, glib was not my intent.
What I’m trying to say, is that
tryis not intended to be a 100% solution. There are various error-handling paradigms that are not well-handled bytry. For instance, if you need to add callsite-dependent context to the error. You can always fall back to usingif err != nil {to handle those more complicated cases.It is certainly a valid argument that
trycan’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,
trywill not handle that case. You’ll have to use old-style error handling. Sorry.”.At this point, I think having
try{}catch{}is more readable 🙃Using named imports to go around
defercorner 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 anotherfunc()to go around the issue, it’s just more things I need to keep in mind, I think it encourages a “bad practice”.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.
I think examples like
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 facttry()is already discouraging wrapping errors given the difficulty of it, but here is an example of what I mean: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.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?
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.
@ianlancetaylor, I think @josharian is correct: the “unmodified” value of
erris the value at the time thedeferis pushed onto the stack, not the (presumably intended) value oferrset bytrybefore returning.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.HandleErrorfwith atryffunction, that prefixs the error with additional context.For example (from a real code I have):
Can be changed to something like:
Or, if I take @agnivade’s example:
However, @josharian raised a good point that makes me hesitate on this solution:
@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
tryintroduced a new change in flow of control that was hard to see, and that althoughtrywas 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.
@kaedys This closed and extremely verbose issue is definitely not the right place to discuss specific alternative syntaxes for error handling.
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.Here’s a
tryhardreport from my company’s 300k-line Go codebase:Initial run:
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-rantryhard. This is an estimate of how many error checks could be rewritten if we didn’t use errgo: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 adopttryfor our codebase.Thanks for the consideration
If anyone wants to try out
tryin 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’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:
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.
baffled and feel bad for the go team lately.
tryis 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
trya variadic built-in if that’s easy to dobecomes
the next verbose thing could be those successive calls to
try.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.
In java where exceptions are the norm, we would have:
Nobody would look at this code and say that we have to do it in 2 lines ie.
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:
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
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
tryhardwill not flag this code as something that can be simplified. But withtryand 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 errorstyle 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.
tryfunction maintains that, and if we see later that there’s no extension necessary, a simple gofix can easily modify code to changetryfunction to a keyword.My 2 cents again!
@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.
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.trycan’t help you write better code, but can produce more bad code.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
@griesemer
Continuing from https://github.com/golang/go/issues/32825#issuecomment-507120860 …
Going along with the premise that abuse of
trywill 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 oftry. 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:
Manual Pass-through, and Automatic Pass-through modes are also simple enough to be agreeable (despite often being a code smell):
However, Manual Indirect and Automatic Indirect modes are both quite disagreeable due to the high likelihood of subtle mistakes:
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.
@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.
@bakul,
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:
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:
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.
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.
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:
with no other use of x anywhere, then it is a valid program transformation to simplify that to
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.
@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”:
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 you see
try(f()), iff()returns an error, thetrywill stop execution of the code and return that error from the function in whose body thetryappears.No. The error is never ignored. It is returned, the same as using a return statement. Like:
The semantics are the same as using a return statement.
Deferred functions run in “in the reverse order they were deferred.”
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
resultexample in https://golang.org/ref/spec#Defer_statements.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.
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.)
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.
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
means
almost everything else should follow from the implications of that definition.
This is not “ignoring” errors. Ignoring an error is when you write:
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:
To answer your question about whether we want to encourage the latter over the former: yes.
@deanveloper
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:
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:
If an extra line is usually required for decoration/wrap, let’s just “allocate” a line for it.
@rogpeppe
Very interesting! . You may really be on to something here.
And how about we extend that idea like so?
BTW, I might prefer a different name vs
try()such as maybeguard()but I shouldn’t bikeshed the name prior to the architecture being discussed by others.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 != nilboilerplate.External handlers need to do this:
which gets used like
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:
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:
That runs afoul of @griesemer’s concern over the ambiguity of
nilhandler funcs and has its owndeferandfunc(err error) errorboilerplate, in addition to having to nameerrin the outer function.If
tryends up as a keyword, then it could make sense to have acatchkeyword, to be described below, as well.Syntactically, it would be much like
handle:Semantically, it would be sugar for the internal handler code above:
Since it’s somewhat magical, it could grab the outer function’s error, even if it was not named. (The
erraftercatchis more like a parameter name for thecatchblock).catchwould have the same restriction astrythat 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
handleproposal, 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
catchthe same as it may require not usingtry.Since these are both sugar, there’s no need to use
catchwithtry. Thecatchhandlers are run whenever the function returns a non-nilerror, allowing, for example, sticking in some quick logging:or just wrapping all returned errors:
@james-lawrence In reply to https://github.com/golang/go/issues/32437#issuecomment-500116099 : I don’t recall ideas like an optional
, errbeing seriously considered, no. Personally I think it’s a bad idea, because it means that if a function changes to add a trailingerrorparameter, existing code will continue to compile, but will act very differently.@brynbellomy Thanks for the keyword analysis - that’s very helpful information. It does seem that
tryas a keyword might be ok. (You say that APIs are not affected - that’s true, buttrymight 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.)
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, whereastry()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 think we can also add a catch function, which would be a nice pair, so:
in this example,
catch()wouldrecover()a panic andreturn ..., 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.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 != nilwould be more compliant with how Go’s exisingiflooks.If we add a rule for the
return ifstatement that when the last condition expression (likeerr != nil) is not present, and the last variable of the declaration in thereturn ifstatement is of the typeerror, then the value of the last variable will be automatically compared withnilas the implicit condition.Then the
return ifstatement can be abbreviated into:return if f, err := os.Open("my/file/path")Which is very close to the signal-noise ratio that the
tryprovides. If we change thereturn iftotry, it becomestry f, err := os.Open("my/file/path")It again becomes similar to other proposed variations of thetryin this thread, at least syntactically. Personally, I still preferreturn ifovertryin this case because it makes the exit points of a function very explicit. For instance, when debugging I often highlight the keywordreturnwithin 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
bodyblock forreturn if, like Original:When debugging:
The meaning of the body block of
return ifis obvious, I assume. It will be executed beforedeferand 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.
@pjebs
This is to address the concerns that
tryproposal 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.
Having to pass an explicit
nilmakes 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.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 iftry(DoSomething())worked, buterr := DoSomething(); try(err)didn’t. Still, it feels liketry(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.
@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.
@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” astryexcept 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.
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 asYou could then do
Which translates to
tryproposal is not consistent with these basic tenets, as it will promote shorthand at the cost of control-flow readability.trybuilt-in a statement instead of a function. Then it is more consistent with other control-flow statements likeif. Additionally removal of the nested parentheses marginally improves readability.deferor 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.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.
%errorin 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
Actually, that can’t be right, because
errwill 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:
The only thing I (and a number of others) wanted to make
tryuseful 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.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.
@pierrec
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).
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.
@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
deferfor error wrapping/decoration has performance implications beyond the simple cost ofdeferitself since functions that usedefercannot be inlined.This means that adopting
trywith error wrapping imposes two potential costs compared with returning a wrapped error directly after anerr != nilcheck:Even though there are some impressive upcoming performance improvements for
deferthe cost is still non-zero.tryhas 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 viadefer.@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.
I ran
tryhardagainst 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:
Some commentary:
50% of all
ifstatements in this codebase were doing error-checking, andtrycould replace ~half of those. This means a quarter of allifstatements in this (small) codebase are a typed-out version oftry.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 runningtryhard, I had predicted 10%).status.Annotatehappens to preservenilerrors (i.e.status.Annotatef(err, "some message: %v", x)will returnnilifferr == 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: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
ifstatements.defer-based error annotation seems somewhat orthogonal totry, to be honest, since it will work with and withouttry. 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: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:
In retrospect, this code demonstrates two issues: first,
http.NewRequestisn’t calling a gRPC API, so usingstatus.Annotatewas 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: doesEdit: answered below, I misread the stats.tryhardhave the right denominator for “non-try candidates”? It looks like its using “try candidates” as the denominator, which doesn’t really make sense.@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!
Thanks @nathanjsweet for your extensive comment - @ianlancetaylor already pointed out that your arguments are technically incorrect. Let me expand a bit:
You mention that the part of the spec which disallows
trywithgoanddefertroubles you the most becausetrywould 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 totryfor that matter (except whentrydoesn’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 maketrywork withgoordefer- it simply suggests that we disallow it; mostly as a practical measure. It’s a “large ask” - to use your words - to maketrywork withgoanddefereven though it’s practically useless.)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. Considernew,make,append,unsafe.Offsetof: they all have specialized rules that we cannot express with an ordinary Go function. Look atunsafe.Offsetofwhich has exactly the kind of syntactic requirement for its argument (it must be a struct field!) that we require of the argument fortry(it must be a single value of typeerroror a function call returning anerroras 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.The proposal also does address what happens when
tryis called with arguments (more than one): It’s not permitted. The design doc states explicitly thattryaccepts an (one) incoming argument expression.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.
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:
This would be the first builtin function of which this is true (you can even
deferandgoapanic) 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
deferandgoapanicis 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 thatdeferandgoalways make sense to use. There are probably lots of non-builtin functions that would never make sense to usedeferorgowith, 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:
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
goanddefer, 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:
tryis called with arguments:I think the reason this isn’t addressed, is because
tryis trying to accept anexprargument 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
trya 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: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
deferandgocases where builtin can’t be used, because results will be disregarded, whereas withtryit can’t even really be said that the function has results.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
tryconstruct 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
trydiffers from that of other languages, then even more confusion will arise.My suggestion is to leave error handling the way it is
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
And definitely I do not want to read the code written by others using the
try… The reason can be two folds:trys 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.
Some people have commented that it’s not fair to count vendored code in
tryhardresults. For instance, in the std library vendored code includes the generatedsyscallpackages which contain a lot of error checking and which may distort the overall picture. The newest version oftryhardnow excludes file paths containing"vendor"by default (this can also be controlled with the new-ignoreflag). Applied to the std library at tip:Now 29% (28.9%) of all
ifstatements appear to be for error checking (so slightly more than before), and of those about 70% appear to be candidates fortry(a bit fewer than before).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.
@cespare Fantastic report!
The fully reduced snippet is generally better, but the parenthesis are even worse than I have expected, and the
trywithin 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):
*Being fair about it, code highlighting would help a lot, but that just seems like cheap lipstick.
@griesemer in case you are looking for more data points. I’ve ran
tryhardagainst 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
tryhardinspected 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
trycould optimise are typically adapters/service wrappers that transparently return the (GRPC) error returned from the wrapped service.Hope this helps.
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
tryfacilitates and then wrestle with potential hazards of the code we are likely to write than it is to run a rather arbitrary line counter.@velovix, re https://github.com/golang/go/issues/32437#issuecomment-503314834:
This is a really nice way to put it. Thanks.
@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
deferstatement and now somebody makes a strong case fordefer.deferdoesn’t have semantics like anything else in the language, the new language doesn’t feel like that pre-deferGo 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 thoseifstatements away for a decade now. And of coursetrycode 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
tryhardrun 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.
Bill expresses my thoughts perfectly.
I can’t stop
trybeing 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).@rsc
What first struck me about
try()— vstryas a statement — was how similar it was in nestability to the ternary operator and yet how opposite the arguments fortry()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
In other languages I frequently improve statements by rewriting them from an
ifto a ternary operator, e.g. from code I wrote today in PHP:Compare to:
As far as I am concerned, the former is much improved over the latter.
#fwiw
@rsc, to your questions,
My package-level handler of last resort – when error is not expected:
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
callerargument – 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 ternaryesquetry(). 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!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:
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:
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:
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.
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
as the way to format that code, and I think it would be quite jarring to try to shift to your example
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.
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.
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.
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 samereturnexpression. Actually,try(withelseclauses) makes it a bit easier to spot when an error handler is doing something special/non-default. Also, tangentially, re: conditionalifexpressions, I think that they bury the lede more than the proposedtry-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
gofmtandgo 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’
tryblocks, 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 existingtype/const/varblocks 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
tryexpressions is a bad idea. So maybe notryblocks at all?On the subject of
tryas 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:Under “Why can’t we use ? like Rust”, the FAQ says:
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:
you’d do:
The semantics of
?()would be very similar to those of the proposedtrybuilt-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 withtry, if the error is non-nil, the?()statement will return it.A brief comment on
tryas a statement: as I think can be seen in the example in https://github.com/golang/go/issues/32437#issuecomment-501035322, thetryburies the lede. Code becomes a series oftrystatements, which obscures what the code is actually doing.I like to emphasize a couple of things that may have been forgotten in the heat of the discussion:
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
ifstatements. (And the request to reduce boilerplate comes from you, the Go community.)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 explicitreturnbecause there’s an explicit handler of sorts, thatreturnstatement 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 explicitreturndoes 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.@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.
@ianlancetaylor
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.
@alexhornbake that gives me an idea slightly different that would be more useful
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.
If you make try a statement, you could use a flag to indicate which return value, and what action:
You still need a sub-expression syntax (Russ has stated it’s a requirement), at least for panic and ignore actions.
I personally liked the earlier
checkproposal more than this, based on purely visual aspects;checkhad the same power as thistry()butbar(check foo())is more readable to me thanbar(try(foo()))(I just needed a second to count the parens!).More importantly, my main gripe about
handle/checkwas that it didn’t allow wrapping individual checks in different ways – and now thistry()proposal has the same flaw, while invoking tricky rarely-used newbie-confusing features of defers and named returns. And withhandleat least we had the option of using scopes to define handle blocks, withdefereven that isn’t possible.As far as I’m concerned, this proposal loses to the earlier
handle/checkproposal in every single regard.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
prefer an explicit return, follows the code is read more than written mantra
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.
adding context with multiple return values
nested functions require that the outer functions handle all results in the same order minus the final error.
compiler refuses compilation due to missing error return value in function
happily compiles because error is explicitly ignored.
compiler is happy. it ignores the error as it currently does because no assignment or else suffix occurs.
within a loop you can use continue.
edit: replaced
;withelsecould we do something like c++ exceptions with decorators for old functions?
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:@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
trymakes 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 totry.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:
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.
@marwan-at-work
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
CopyFiletakes ~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.
@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, haveWith 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
deferapplied to every exit in the function body, otherwisetryhas 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:
My understanding is that it would desugar into
which fails to compile because
erris 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.@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 ofcheckwould only allow to augment errors through something like afmt.Errorflike API (as part of thecheck), 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
trymakes sense for code that now looks basically like this: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
deferis not right, one can still use anifstatement.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:
The code above seems cleaner than the initial comment. I wish I could purpose this.
I made a new proposal #35179
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’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.
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.
#jmtcw
awesome,quite helpful
Excellent!
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?
I was just pondering over a piece of my own code, and how that’d look with
try:Could become:
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.
@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
tryis simply syntactic sugar for a common pattern (in contrast togowhich implements a major feature of Go and cannot be implemented with other Go mechanisms), likeappend,copy, etc. Using a built-in is a fine choice.(But as I have said before, if that is the only thing that prevents
tryfrom being acceptable, we can consider making it a keyword.)How about use bang(!) instead of
tryfunction. This could make functions chain possible:Is nobody bothered by this type of use of try:
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.
Here are the statistics for a small GCP helper tool to automate user and project creation:
After this, I went ahead and checked all the places in the code that are still dealing with an
errvariable to see if I could find any meaningful patterns.Collecting
errsIn 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
tryor some form of support for multi-errors is added to Go itself.Error Decoration Responsibility
After having read this comment again, there were suddenly a lot of potential
trycases 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:Quoting the important part from the Go blog here again here for clarity:
With this in mind, the above code now becomes:
At first glance, this seems like a minor change but in my estimation, it could mean that
tryis actually incentivising to push better and more consistent error handling up the function chain and closer to the source or package.Final Notes
Overall, I think the value that
tryis bringing long-term is higher than the potential issues that I currently see with it, which are:tryis changing control flow.trymeans you can no longer put a debug stopper in thereturn errcase.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.
Update
Fixed a function signature that didn’t return an
errorbefore. Thank you @magical for spotting that!@balasanjay (and @lootch): Per your comment here, yes, the program https://play.golang.org/p/KenN56iNVg7 will print 1.
Since
tryis 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
tryproposal. Any of the proposed alternatives that don’t require an explicitreturnwill have to answer the same question.]Regarding your example of a writer
n += try(wrappedWriter.Write(...)): Yes, in a situation where you need to incrementneven in case of an error, one cannot usetry- even iftrydoes not zero non-error result values. That is becausetryonly returns anything if there is no error:trybehaves 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 oftry.But in cases like your example one also would have to be careful with an
ifstatement and make sure to incorporate the returned byte count inton.But perhaps I am misunderstanding your concern.
@neild Even if
VolumeCreatewould decorate the errors, we would still needCreateDockerVolumeto add its decoration, asVolumeCreatemay be called from various other functions, and if something fails (and hopefully logged) you would like to know what failed - which in this case isCreateDockerVolume, Nevertheless, ConsideringVolumeCreateis a part of the APIclient interface.The same goes for other libraries -
os.Opencan well decorate the file name, reason for error etc, butfunc ReadConfigFile(...func WriteDataFile(...etc - callingos.Openare 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.@daved
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
tryis “special”.@mattn that’s the thing though, theoretically you are absolutely correct. I’m sure we can come up with cases where
trywould 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
tryinto the language.As an aside, I still wouldn’t use your style in my code. I’d write it as
so I still would be saving about the same amount of typing per instance of those n1/n2/…n(n)s
tryhardagainst a large Go API which I maintain with a team of four other engineers full time. In 45580 lines of Go code,tryhardidentified 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.trywould 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 writingif 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 theerritself.trywas variadic if you wanted to optionally add context liketry(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.@sirkon Since
tryis special, the language could disallow nestedtry’s if that’s important - even iftrylooks like a function. Again, if this is the only road block fortry, 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.Yep,
tryis the way to go. I’ve tried to addtryonce, 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/Thanks for your note, @ugorji.
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.
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
AS opposed to:
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?
@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:
It was interesting to see to say the least. It was my first time seeing
deferused 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 likefmt.HandleErrorfdoes help it feel much nicer. I still feel negatively towards it, though.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.)
@dominikh asked:
So much about the try proposal is undecided, up in the air, unknown.
But this question I can answer definitively: no.
1.13 defers will be about 30% faster:
This is what I get on @networkimprov 's tests above (1.12.5 to tip):
(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.
I like the
try a,b := foo()instead ofif 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 thanif err!=nil {...}(it will be very difficult to find better) ?I think the optional
elseintry ... 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 bygo fmtand I expect this behavior will be kept fortry else. So we should be discussing and comparing samples where theelseblock is on a separate line. But even then I am not sure about the readability ofelse {at the end of the line.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.
Passing the result of a function call to
tryas intry(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.
The proposal also raises the possibility of extending
trywith extra arguments.Suppose we want to add a handler argument. It can either go at the beginning or end of the argument list.
Putting it at the beginning doesn’t work, because (given the semantics above)
trywouldn’t be able to distinguish between an explicit handler argument and a function that returns a handler.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
tryfeel inconsistent with the rest of the language, and so i’m not suretrywould be easy to extend in the future as the proposal states.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 <function-call>” syntax beautifully captured starting a concurrent thread. I don’t get that sort of “aha!” feeling here. It doesn’t feel right. The cost/benefit ratio is not large enough.
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
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?
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
http2Framerexample: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 tryis not allowed.That
http2Framerexample could instead be: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:
That specific
http2Framerexample ends up being not as short as it possibly could be. However, it holds returning from a function more “sacred” if thetrymust be the first thing on a line.@crawshaw mentioned:
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 iftryis required to be the first thing on a line?@james-lawrence
Taking your example:
Which compares to what we do today:
Is definitely preferable to me. Except it has a few issues:
errappears to be “magically” declared. Magic should be minimized, no? So let’s declare it:nilvalues asfalsenor pointer values astrue, so it would need to be: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()anderror()? Then we could do this, which does not feel as bad to me:Or better (something like):
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…
@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
returnand leave the function. What it does not contemplate is issuing abreakout of the currentforor acontinuefor the currentfor.Early
returns are a sledgehammer when many times a scalpel is the better choice.So I assert
breakandcontinueshould be allowed to be valid error handling strategies and currently your proposal presumes onlyreturnwhereastry()presumes that or calling an error handler that itself can onlyreturn, notbreakorcontinue.@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
trythat changes the flow control in some circumstances. Generics will never change the flow of control. So I really don’t see the relationship.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
Then, in a function
gwhere the last result parameter has typeerror(i.e., any function wheretrymy be used), a call tofmay be deferred as follows:The semantics of error-defer are:
fis called with the last result parameter ofgas the first input parameter offfis only called if that error is not nilfis assigned to the last result parameter ofgSo to use an example from the old error-handling design doc, using error-defer and try, we could do
Here’s how HandleErrorf would work:
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:
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
tryencourages more use ofdeferfor error manipulation, it makes some sense thatdefercould be extended to better suit it to that purpose. Also, there’s a certain symmetry betweentryand 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:
With error-defer, this becomes:
While repetitive
if 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 forif err !=nil { return err }:for
And if there is more than one return value,
try(err)couldreturn t1, ... tn, errwhere 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.
@griesemer - IIUC, you are saying that for callsite-dependent error contexts, the current if statement is fine. Whereas, this new
tryfunction 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.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.
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”.
Thecheck-handleidea 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.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.@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 theerr != nils, but to metryis 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’m sure there are other things you can do in the defer, but regardless,
tryis 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.@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.
In my opinion and experience this use case is a small minority and doesn’t warrant shorthand syntax.
Also, the approach of using
deferto handle errors has issues in that it assumes you want to handle all possible errors the same.deferstatements can’t be canceled.What if I want different error handling for errors that might be returned from
foo()vsfoo2()?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
checkwith format to avoid naming the return, you can just do that with a function that checks for nil, like soThis can be used without a named return like so:
The proposed fmt.HandleError could be put into the deferred package instead and my errors.Defer helper func could be called
deferred.Execand there could be a conditional exec for procedures to execute only if the error is non-nil.Putting it together, you get something like
Another example:
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].
Good point. A simpler example:
@buchanae interesting. As written, though, it moves fmt-style formatting from a package into the language itself, which opens up a can of worms.
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)
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.
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
trywere 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
deferor evengoto) which I hadn’t previously considered and I do hope the Go team will at least consider changinggo fmtto allow single statementif’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 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
deferperformance 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.@jonbodner
This works (https://play.golang.org/p/NaBZe-QShpu):
Ah, the first downvote! Good. Let the pragmatism flow through you.
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 ?
@iand Indeed - that was just an oversight of mine. My apologies.
We do believe that
trydoes allow us to write more readable code - and much of the evidence we have received from real code and our own experiments withtryhardshow significant cleanups. But readability is more subjective and harder to quantify.@griesemer can you elaborate on the specific metrics the team will be using to establish the success or failure of the experiment?
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.:however, folks who want a more flexible situation can do something like:
One thing that this idea does is make the word
tryless appropriate, but it keeps backwards compatibility.Executed
tryhardon our codebase and this is what we got:First, I want to clarify that because Go (before 1.13) lacks context in errors, we implemented our own error type that implements the
errorinterface, some functions are declared as returningfoo.Errorinstead oferror, 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 withmake([]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
xerrorsimpl that’s coming anyways.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:
@griesemer: I am suggesting that it is better to set the other return values to their zero values, because then it is clear what
trywill 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,
trywill 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 valuestryis 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
tryin 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 thattrybe specified to always return the zero values of the non-error argument.This is a somewhat interesting example. My first reaction when looking at it was to ask whether this would produce stuttering error strings like:
The answer is that it doesn’t, because the
VolumeCreatefunction (from a different repo) is: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
VolumeCreatefunction really should be decorating its errors. In that case, however, it’s not clear to me that theCreateDockerVolumefunction should add additional decoration, since it has no new information to provide.@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 writedefer 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.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.
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 whattrywould 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
trywould 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.
@olekukonko, re https://github.com/golang/go/issues/32437#issuecomment-503508478:
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 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.)
@rsc I also think there is no problem with current error code. So, please, count me in.
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
@brynbellomy wrote:
The unique goal of the
trybuilt-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.
In response to https://github.com/golang/go/issues/32437#issuecomment-502837008 (@rsc’s comment about
tryas 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
tryas an expression look much better than the ones withtryas a statement. The fact that the statement leads withtrydoes in fact make it much harder to read. However, I am still worried that people will nest try calls together to make bad code, astryas an expression really encourages this behavior in my eyes.I think I would appreciate this proposal a bit more if
golintprohibited nestedtrycalls. I think that prohibiting alltrycalls inside of other expressions is a bit too strict, havingtryas 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.
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
trycalls would also help with the “debuggability” issue, since it’s much easier to expand atryinto anifif it’s on the outside of an expression.Again, I’d almost like to say that a
tryinside a sub-expression should be flagged bygolint, but I think that might be a little too strict. It would also flag code like this, which in my eyes is fine:This way, we get both the benefits of having
tryas an expression, but we aren’t promoting adding too much complexity to the horizontal axis.Perhaps another solution would be that
golintshould only allow a maximum of 1tryper 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 somegolintstandards related to it.@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.
@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.
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
tryas 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:
… 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.
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.
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.)
Isn’t this the entire reason Go doesn’t have a ternary operator?
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.
@deanveloper
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:
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.
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 ABSOLUTELY not a change to the spec. It is a request for a switch to change the behavior of the
buildcommand, not a change in language spec.If someone asks for the
gocommand to have a switch to display its terminal output in Mandarin, that is not a change to the language spec.Similarly if
go buildwere to sees this switch then it would simply issue an error message and halt when it comes across atry(). No language spec changes needed.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.
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-guardused to disallow problematic features during compilation, starting with disallowingtry().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 ontry()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.
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
@mikeschinkelReinstalled 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
golintand 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
trythrough the compiler, you’d be turning offtryfor every single one of the libraries you are using as well. It’s just a bad idea to have it be a compiler flag.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.
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
trybuiltin would therefore be a compiler flag that would change the spec of the language being compiled.That being said, I think that
ImportPathshould be standardized in the spec. I may make a proposal for this.While this is true, you would not want the implementation of
tryto 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.@mikeschinkel Wouldn’t it be just as easy to forget to turn on the compiler option in that situation?
This would have to be on a linting tool, not on the compiler IMO, but I agree
@owais
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.
@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 thereturn?@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,
trycouldn’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 simplifytryto 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 writetry(a, b, c, handle).)An earlier version of
trydefined 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 typeerror, and that value is tested against nil. (etc. - the rest you can imagine).Anyway, the point is that
trysyntactically accepts only one, or perhaps two expressions. (But it’s a bit harder to describe the semantics oftry.) The consequence would be that code such as:would not be permitted anymore. But there is little reason for making this work in the first place: In most cases (unless
aandbare named results) this code - if important for some reason - could be rewritten easily into(or use an
ifstatement as needed). But again, this seems unimportant.echoing what @ubombi said:
In Ruby, procs and lambdas are an example of what
trydoes…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
trydoes…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
tryfunction by introducingproc functionsI still prefer
if err != nil, because it’s more readable but I thinktrywould be more beneficial if the user defined their own proc:And then you can call it:
The benefit here, is that you get to define error handling in your own terms. And you can also make a
procexposed, 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()andproc!()so that a programmer knows that a proc call might actually return out of the calling function.@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, thenreturn. If we shell out to a local function in theelseclause, we probably lose that:(I still prefer this to compound ifs, though)
However, you could still get one-liner returns that add error context by implementing a simple
gofmttweak discussed earlier in the thread:@josharian Yes, this is more or less exactly the version we discussed last year (except that we used
checkinstead oftry). I think it would be crucial that one couldn’t jump “back” into the rest of the function body, once we are at theerrorlabel. That would ensure that thegotois somewhat “structured” (no spaghetti code possible). One concern that was brought up was that the error handler (theerror:) 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.One objection to
tryseems 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):The pgStore example:
@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 hiddentry(f(), true)that is easy to overlook, and I don’t think the builtin functions should encourage panics.@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 Result<T, error>, to loosely borrow from Rust’s terminology.
tryis a utility that unpacks the result and either returns an “error result” iferror != nil, or unpacks the T portion of the result iferror == nil.Of course, in Go we don’t actually have result objects, but it’s effectively the same pattern and
tryseems 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, andtrys take on it seems reasonable to me. Myself and others are suggesting to extend the capability oftrya bit to better fit existing Go error handling patterns, but the underlying concept remains the same.@zeebo Thanks for this example. It looks like using an
ifstatement is exactly the right choice in this case. But point taken, formatting the if’s into one-liners may streamline this a bit.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.
These are valuable thoughts about requiring
gofmtto formattry, but I’m interested if there are any thoughts in particular ongofmtallowing theifstatement checking the error to be one line. The proposal was lumped in with formatting oftry, but I think it’s a completely orthogonal thing. Thanks.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:
We can:
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:
or
or
and use this all around my my code:
So, the actual need would be to develop a trigger when err.Error is set to not nil Using this method we can also:
Which would tell the calling function to continue instead of return
And use different error handlers in the same function:
etc.
Going over the goals again
to
and actually - better readability (IMO, again)
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…
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 😦@josharian
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.Exitis a library-call, so Go implementations are free just not to provide it. The shape ofmainis a language concern though.A question about the proposal that is probably best answered by “nope”: How does
tryinteract 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: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.
If defer-based error handling is going to be A Thing, then something like this should probably be added to the errors package:
Ignoring the errors of deferred Close statements is a pretty common issue. There should be a standard tool to help with it.
maybe we can add a variant with optional augmenting function something like
tryfwith this semantics:translates this
into this
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 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.I don’t follow this line:
It drops the inbound error on the floor, which is unusual. Is it meant to be used something more like this?
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.
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 returnsCould 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 returnsseemed 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.Maybe someone should lock this issue? The discussion is probably better suited elsewhere.
@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:
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:
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
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.
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?
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
vetlimitations will be imposed, as of right now you’ve said vet will be the defense against unreasonable try’s, but how will that materialize?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.
@trende-jp
Can it not be solved with
defer?Line numbers in error messages can also be solved as I’ve shown in my blog: How to use ‘try’.
@griesemer Returning
errorinstead 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 usingtrywouldn’t be worth trading those compile-time checks for run-time checks.Ran
tryhardon some of my codebases. Unfortunately, some of my packages have0try 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 emitSanitizedErrors rather thanerrors 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:
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.
@fabstu The
deferhandler will work in your example just fine, both with and withouttry. Expanding your code with enclosing function:(note that the result
errwill be set by thereturn err; and theerrused by thereturnis the one declared locally with theif- these are just the normal scoping rules in action).Or, using a
try, which will eliminate the need for the localerrvariable:And most probably, you’d want to use one of the proposed
errors/errdfunctions:And if you don’t need wrapping it will just be:
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?
In case you have many lines like these (from https://github.com/golang/go/issues/32437#issuecomment-509974901):
You could use a helper function that only returns a non-nil error if some condition is true:
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.
@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:
Much of the code uses an idiom similar to:
It might be interesting if
tryhardcould 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 commondeferhandler.@dpinela The compiler already translates a
switchstatement such as yours as a sequence ofif-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.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 != nilalone” with 90% thumb-up.I agree that
os.Exitis the odd one out, but it has to be that way.os.Exitstops all goroutines; it wouldn’t make sense to only run the deferred functions of just the goroutine that callsos.Exit. It should either run all deferred functions, or none. And it’s much much easier to run none.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. 😃
@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 iftryshould be a built-in macro predeclared in the universe block, or not. It would be logical for those opposed totryto be even more opposed to hygienic macros 😉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.”
This is good, I think go team really should take this one. This is better than try, more clearly !!!
About the example with
CreateDockerVolumehttps://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 usetryand add context indeferfor the whole function.I tried to mimik this by adding an error handler function at the start, it’s working fine:
That will look fine and idiomatic with
try+defer@beoran I did some initial analysis of the Go Corpus (https://github.com/rsc/corpus). I believe
tryhardin its current state could eliminate 41.7% of allerr != nilchecks in the corpus. If I exclude the pattern “_test.go”, this number rises to 51.1% (tryhardonly 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 performerr != nilchecks) by using a hacked-up version oftryhard, and ideally we’d wait untiltryhardreported these statistics itself.Also, if
tryhardwere to become type-aware, it could theoretically perform transformations like this:This takes advantage of errors.Wrap’s behavior of returning
nilwhen the passed in error argument isnil. (github.com/pkg/errors is also not unique in this respect, the internal library I use for doing error wrapping also preservesnilerrors, 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 != nilchecks 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.@jonbodner https://go-review.googlesource.com/c/go/+/182717 gives you a pretty good idea of what that might look like.
I had a thought while talking about
trywith a co-worker. Maybetryshould 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 usingtryas 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 usefultryhardis, 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
tryaffects 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.
@neild thanks.
I don’t want to derail this thread, but…
The problem is, as the author of the
CreateDockerVolumefunction i may not know whether the author ofVolumeCreatehad 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.@pierrec Ok, let’s change it:
@reusee And why is it better than this?
In what moment did we all decide that shortness better than readability?
@makhov
Not really. Errors are still values in this proposal.
try()is just simplifying the control flow by being a shortcut forif err != nil { return ...,err }. Theerrortype is already somehow “special” by being a built-in interface type. This proposal is just adding a built-in function that complements theerrortype. There is nothing extraordinary here.Verbosity in error handling is a good thing in my opinion. In other words I don’t see a strong use case for try.
@cespare A decoder can also be a struct with an error type inside it, with the methods checking for
err == nilbefore every operation and returning a boolean ok.Because this is the process we use for codecs,
tryis absolutely useless because one can easily make a non magic, shorter, and more succinct idiom for handling errors for this specific case.@networkimprov Agreed - similar suggestions have been brought up before. I’ll try to find some time over the next days to improve this.
@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
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:
So that’s actually fine, but then you might see this and think to rewrite it to
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.
Another reason I can see people not liking it is that when one writes
return ..., err, it looks likeerrshould be returned. But it doesn’t get returned, instead the value is modified before sending. I have said before thatreturnhas always seemed like a “sacred” operation in Go, and encouraging code that modifies a returned value before actually returning just feels wrong.Defers
The primary change from the Gophercon check/handle draft to this proposal was dropping
handlein favor of reusingdefer. Now error context would be added by code like this deferred call (see my earlier comment about error context):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()anddefer 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:
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:
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 != nilstatements 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.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:
(
closedandremovedare 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
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
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:
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?
@mikeschinkel I’m not the Carl you’re looking for.
@deanveloper wrote:
I agree that deeply nested
trycould be hard to read. But this is also true for standard function calls, not just thetrybuilt-in function. Thus I don’t see whygolintshould forbid this.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 thannet/httpin 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 😃@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.
@networkimprov, re https://github.com/golang/go/issues/32437#issuecomment-502879351
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.
I think that form ends up recreating existing control structures.
For a visual idea of
tryin the std library, head over to CL 182717.Here’s an interesting
tryhardfalse negative, fromgithub.com/josharian/pct. I mention it here because:trydetection is trickyif err != nilimpacts how people (me at least) structure their code, and thattrycan help with thatBefore:
After (manual rewrite):
@mikeschinkel,
No. This is not about error handling but about deferred functions. They are not always closures. For example, a common pattern is:
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:
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:
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 errinside the closure body, the compiler will tell you “too many arguments to return”.So, no, writing
return errdoes 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.@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.@magical You’re feedback has been addressed in the updated version of the detailed proposal.
@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.
@deanveloper
I must admit not readable for me, I would probably feel I must:
or similar, for readability, and then we are back with a “try” at the beginning of every line, with indentation.
@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.
Just interjecting a specific comment since I did not see anyone else explicitly mention it, specifically about changing
gofmtto support the following single line formatting, or any variant:Please, no. If we want a single line
ifthen please make a single lineif, e.g.:But please, please, please do not embrace syntax salad removing the line breaks that make it easier to read code that uses braces.
@thepudds
Perhaps its too deep a burn in my brain cells, but this hits me too much as a
and a slippery slope to a
I think you can see where that’s leading, and for me comparing
with
(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” .
at least explicitly sets the variable to return, and let me handle it within the function, whenever I want.
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.
Here’s @brynbellomy’s example rewritten with the
tryfunction, using avarblock to retain the nice alignment that @thepudds pointed out in https://github.com/golang/go/issues/32437#issuecomment-500998690.It’s as succinct as the
try-statement version, and I would argue just as readable. Sincetryis 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
tryworks in avarblock, though. I assume each line of thevarcounts as a separate statement, rather than the whole block being a single statement, as far as the order of what gets assigned when.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
Is highly readable. And since I believe
Is almost as readable, yet had it’s scope “issues”, perhaps a
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:
Expression
try-elseis a ternary operator.Statement
try-elseis anifstatement.Builtin
trywith an optional handler can be achieved with either a helper function (below) or not usingtry(not pictured, we all know what that looks like).All three cut down on the boilerplate and help contain the scope of errors.
It gives the most savings for builtin
trybut that has the issues mentioned in the design doc.For statement
try-else, it provides an advantage over usingifinstead oftry. 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 eachelseblock that’s a bit repetitious: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
trysimple and just not usetryin those situations? On the other hand, if it’s more like 1 out of 10 times, addingelse/handler seems more reasonable.It would be interesting to see an actual distribution of how often
trywithout anelse/handler vstrywith anelse/handler would be useful, though that’s not easy data to gather.While I like the try-else statement, how about this syntax?
Like many others here, I prefer
tryto 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
guardlike in Swift and make theelseclause optional? Something likewhere
Identifieris the error variable name bound in the followingBlock. With noelseclause, just return from the current function (and use a defer handler to decorate errors if need be).I initially didn’t like an
elseclause because it’s just syntactic sugar around the usual assignment followed byif err != nil, but after seing some of the examples, it just makes sense: usingguardmakes the intent clearer.EDIT: some suggested to use things like
catchto somehow specify different error handlers. I findelseequally viable semantically speaking and it’s already in the language.Regarding
try else, I think conditional error functions likefmt.HandleErrorf(edit: I’m assuming it returns nil when the input is nil) in the initial comment work fine so addingelseis unnecessary.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
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.
Since my previous post in support of the proposal, I’ve seen two ideas posted by @jagv (parameterless
tryreturns*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:#1 would simply return a pointer to the error return parameter (ERP) or nil if there wasn’t one (#4 only). This would provide an alternative to a named ERP without the need to add a further buult-in.
#2 would work exactly as currently envisaged. A non-nil error would be returned immediately but could be decorated by a
deferstatement.#3 would work as suggested by @josharian i.e. on a non-nil error the code would branch to the label. However, there would be no default error handler label as that case would now degenerate into #2.
It seems to me that this will usually be a better way of decorating errors (or handling them locally and then returning nil) than
deferas 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
gotobehavior 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
panicas this would conflict with #4.#4 would
panicimmediately rather than returning or branching. Consequently, if this were the only version oftryused 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
tryas the alternative of branching to an error handler and then panicking from that would still require an ERP.Here’s another concern with using defers for error handling.
tryis 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: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
recordMetriccalls. 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.
@ianlancetaylor
When trying to formulate an answer to your question, I tried to implement the
tryfunction in Go as it is, and to my delight, it’s actually already possible to emulate something quite similar:See here how it can be used: https://play.golang.org/p/Kq9Q0hZHlXL
The downsides to this approach are:
tryas 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 ofsuper(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.tryI 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: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.
@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
trydiscourages people from decorating errors (they are already discouraged even with theiffor the above reason); it’s thattrydoesn’t encourage it.(One way of encouraging it is to tie it into
try: One can only usetryif 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
deferjust to make it work better fortryis not something we would want to consider unless thosedeferchanges are beneficial in a more general way. Also, your suggestion tiesdefertogether withtryand 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
deferjust so they can usetry. But without doing that, we’re back to square one: people will lean towards not decorating errors.@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
trywith an explicit error handler is a better idea than the current minimaltry. 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
tryas is, we still can move towards atrywith a handler in a forward-compatible way - at least if the handler is optional. But let’s take one step at a time.)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
returnkeyword, 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
tryfcounter-proposal (that’s been posted independently twice in this thread) nor thetry(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:
defercan only be a function or method call. Allowdeferto also have a function name or a function literal, i.e.When panic or deferreturn encounter this type of defer they will call the function passing the zero value for all their parameters
Allow
tryto have more than one parameterWhen
tryencounters the new type of defer it will call the function passing a pointer to the error value as the first parameter followed by all oftry’s own parameters, except the first one.For example, given:
the following will happen:
The code in https://github.com/golang/go/issues/32437#issuecomment-499309304 by @zeebo could then be rewritten as:
And defining ErrorHandlef as:
would give everyone the much sought after
tryffor free, without pullingfmt-style format strings into the core language.This feature is backwards compatible because
deferdoesn’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:trymatch the signature of the functions passed todefertryto copy its arguments into the arguments of the deferred call when it encounters a call deferred by the new kind of defer.@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.
@velovix We quite liked the idea of a
trywith 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 benil, to make it explicit we don’t care about handling the error. Now what happens if one provides a function value (not anilliteral), and that function value (stored in a variable) happens to be nil? By analogy with the explicitnilvalue, 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 thedefer. There’s probably more.@pjebs That is exactly what we considered in a prior proposal (see detailed doc, section on Design iterations, 4th paragraph):
The (Go Team internal) consensus was that it would be confusing for
tryto 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).@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.
The design says you explored using
panicinstead 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 forMustXXX( )convention)For unit tests, a modest language change.
@griesemer
Quite the opposite: I’m suggesting that
goplscan optionally write the error handling boilerplate for you.As you mentioned in your last comment:
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:
Then the user triggers the tool, perhaps by just saving the file (similar to how gofmt/goimports typically work) and
goplswould look at this function, analyze its return signature and augments the code to be this: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:
goplscan understand that the block exists, and it wouldn’t modify it.@Goodwine
As noted in the proposal (and shown by example),
trydoesn’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
trywon’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 ofHandleErrorfcovers a large area of use because only adding function-wide context to errors is not unusual.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
trywould 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.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. ifxerrorsbecomeserrors, errors should have a something that looks like a stack trace thattry()could add, no? If so maybe that would be enough 🤔Building on @Goodwine’s impish point, you don’t really need separate functions like
HandleErrorfif you have a single bridge function likewhich you would use like
You could make
handleritself a semi-magic builtin liketry.If it’s magic, it could take its first argument implicitly—allowing it to be used even in functions that don’t name their
errorreturn, 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: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, butdefer handlerreads quite nicely.Since it uses
deferit could call itshandlefunc whenever there was a non-nil error being returned, making it useful even withouttrysince you could add aat the top to
fmt.Errorf("mypkg: %w", err)everything.That gives you a lot of the older
check/handleproposal but it works with defer naturally (and explicitly) while getting rid of the need, in most cases, to explicitly name anerrreturn. Liketryit’s a relatively straightforward macro that (I imagine) could be implemented entirely in the front end.@natefinch Absolutely agree.
I wonder if a rust style approach would be more palatable? Note this is not a proposal just thinking through it…
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.@elagergren-spideroak hard to say that
tryis 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
than your example.
trydoing nothing is the happy path, so that looks as expected. In the unhappy path all it does is return. Seeing there is atryis enough to gather that information. There isn’t anything expensive about returning from a function either, so from that description I don’t thinktryis doing a 180@qrpnxz
(yes I understand there is
ReadFileand 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
defererror 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.I like the proposal but I think that the example of @seehuhn should be adressed as well :
would return the error from Close() only if the error was not already set. This pattern is used so often…
@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
tryshouldn’t be the next drama.@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.
trydoesn’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 mycheckexample 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
newScannerandRunMigrationsdon’t themselves provide messages that clue you into which one went wrong, then you’re left guessing.The detailed proposal is now here (pending formatting improvements, to come shortly) and will hopefully answer a lot of questions.
@ianlancetaylor
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.
@griesemer, you announced the decision to decline the
tryproposal, 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.)?
@griesemer @ianlancetaylor @rsc Do you still plan to address error handling verbosity, with another proposal solving some or all of the issues raised here?
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.
@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.
@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.
@jonbodner example, and the way @griesemer rewrote it, is exactly the kind of code I have where I’d really like to use
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 writereturnw/o the need to specify the actual results with thereturn. In general (but not always!) such practice makes it harder to read and reason about the code because one can’t simply look at thereturnstatement 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.
trywill 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
tryalways sets the result error (which is unnamed in this case). But you are declaring a different local variableerrand theerrd.Wrapoperates on that one. It won’t be set bytry.@fastu And finally, you can use
errors/errdalso withouttryand then you get:@lpar You’re welcome to discuss alternatives but please don’t do this in this issue. This is about the
tryproposal. 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.@mrkanister Nitpicking, but you can’t actually use
tryin this example becausemaindoes not return anerror.@networkimprov I can confirm that at least for
gormthese are results from the latesttryhard. The “non-try candidates” are simply not reported in the tables above.@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
deferto decorate errors is possible with or withouttry. 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 adefer. 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).
tryensures exactly that. I don’t see any “abuse” here.@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.
@nathanjsweet That was already considered (in fact it was an oversight) but it makes
trynot 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
tryargument are really a non-issue - virtually every built-in has special rules.@griesemer
Can you point this out. I was surprised to read this.
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
tryproposal 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:panicandos.Exitdo)unsafe.Offsetofas 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.Offsetofrequires one argument, whereastryrequires 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 liketry(os.Open("/dev/stdout")...)).@nathanjsweet Some of what you say turns out not to be the case. The language does not permit using
deferorgowith the pre-declared functionsappend cap complex imag len make new real. It also does not permitdeferorgowith the spec-defined functionsunsafe.Alignof unsafe.Offsetof unsafe.Sizeof.@nathanjsweet the proposal you seek is #32611 😃
@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(...))evaluateswrap(...)first right? How much of that do you think gets optimised away by the compiler? (Compared to just runningif 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.
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 forif err != nil { return _x_ }, explicitly named to indicate it will return)So the common cases could be one nice short, explicit line:
Instead of the 3 lines they are now:
Things I Disagree with:
reterr(every person who writes Go would benefit from reterr).Here was an interesting talk from Rust linked on Reddit. Most relevant part starts at 47:55
@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.
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?
@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
!(ietry!(...)andprintln!(...)).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.
@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
@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
tryor 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.In my see, I want to try a block code, now
trylike a handle err funcI like the overall idea of reusing
deferto address the problem. However, I’m wondering iftrykeyword is the right way to do it. What if we could reuse already existing pattern. Something that everyone already knows from imports:Explicit handling
Explicit ignoring
Deferred handling
Similar behaviour to what
tryis going to do.@griesemer
The design doc currently has the following statements:
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
trybehave 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 totryitself.Given that this property of naked returns (non-locality) is generally disliked, what are your thoughts on having
tryalways 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 ofio.Writer, which need to return a count of bytes written, even in the partial write situation. That said, it seems liketryis error-prone in this case anyways (e.g.n += try(wrappedWriter.Write(...))does not setnto the right number in the event of an error return). It seems fine to me thattrywill 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 alltry-generated blocksgototo a common shared function-wide label, which zeroes the non-error output values.Also, as I’m sure you’re aware,
tryhardis already implemented this way, so as a side benefit this will retroactively maketryhardmore correct.@makhov You can make it more explicit:
@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
erris 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.
@ngrilly Simplifying? How?
How I should understand that error was returned inside loop? Why it’s assigned to
errvar, not tofoo? Is it simpler to keep it in mind and not keep it in code?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.
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:
@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.
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 withif err != nilin mind, so it was designed around it (Rob Pike’s “errors are values” article on avoiding repeating comes to mind).@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…
@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 == nilprovides a legitimate test for some early returns.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
tryas 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.
Try must not be a function to avoid that damned
file leak.
I mean
trystatement will not allow chaining. And it is better looking as a bonus. There are compatibility issues though.@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 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
Have have had managed several
bad written codebefore and I can testify it’s the worst nightmare for every developer.The fact the documentation says to use
Alikes does not mean it would be followed, the fact remains if it’s possible to useAA,ABthen there is no limit to how it can be used.To my surprise, people already think the code below is cool… I thinkit's an abominationwith all due respect apologies to anyone offended.Wait until you check
AsCommitand you seeThe 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
Am in favour of
Example 2with a littleelse, Please note that this might not be the best approach howeverabominationthe others can give birth totrydoesn’t behave like a normal function. to give it function-like syntax is little of.gousesifand if I can just change it totry tree := r.LookupTree(treeOid) else {it feels more naturaltry&catchOnce again I want to apologise for being a little selfish.
@beoran
tryhardis very rudimentary at the moment. Do you have a sense for the most common reasons whytrywould be rare in your codebase? E.g. because you decorate the errors? Because you do other extra work before returning? Something else?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 oftry()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 oftry()because of this.@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 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.
@bakul because arguments are evaluated immediately, it is actually roughly equivalent to:
This may be unexpected behavior to some as the
f()is not defered for later, it is executed right away. Same thing applies togo try(f()).@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.
The tools
tryhardis very informative ! I could see that i use oftenreturn ...,err, but only when i know that i call a function that already wrap the error (withpkg/errors), mostly in http handlers. I win in readability with fewer line of code. Then in theses http handler i would add adefer 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.Printfand i will do it withtry. Will it be possible for example to dodefer try(f.Close())?So, maybe
trywill finally help to add context and push best practice rather than the opposite.I’m very impatient to test in real !
@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?
@beoran i believe this proposal make further improvement possibles. Like a decorator at last argument of
try(..., func(err) error), or atryf(..., "context of my error: %w")?breakdoes break the loop.continuedoes continue the loop, andgotodoes 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 thinkcheckis 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}Re swtch.com/try.html and https://github.com/golang/go/issues/32437#issuecomment-502192315:
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.
@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.
This is where using
breakinstead ofreturnshines with 1.12. Use it in afor range once { ... }block whereonce = "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 ofbreak. And if you need to decorate all errors you do it just before the solereturnat 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.
#fwiw
@griesemer One area that might be worth expanding a bit more is how transitions work in a world with
try, perhaps including:vetorstaticcheckor similar, vs. © 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.Stages of error decoration
This is not exhaustive, but a representative set of stages could be something like:
0. No error decoration (e.g., using
trywithout any decoration). 1. Uniform error decoration (e.g., usingtry+deferfor 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
trywithout any decoration in stage 0, you can add something likedefer 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 thedefer(to avoid double decoration), then it seems one would need to visit all the return points to desugar thetryuses intoifstatements, 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.
Mistakes when transitioning between decoration styles
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
CopyFilefunction has 4tryuses, including in this section:If someone did an “obvious” manual desugaring of
w := try(os.Create(dst)), that one line could be expanded to:That looks good at first glance, but depending on what block that change is in, that could also accidentally shadow the named return parameter
errand break the error handling in the subsequentdefer.Automating transitioning between decoration styles
To help with the time cost and risk of mistakes, perhaps
gopls(or another utility) could have some type of command to desugar a specifictry, or a command desugar all uses oftryin a given func that could be mistake-free 100% of the time. One approach might be anygoplscommands only focus on removing and replacingtry, but perhaps a different command could desugar all uses oftrywhile also transforming at least common cases of things likedefer fmt.HandleErrorf(&err, "copy %s %s", src, dst)at the top of the function into the equivalent code at each of the formertrylocations (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.Idiomatic results?
A related comment is it is not immediately obvious is how frequently a programmatic mistake free transformation of a
trywould end up looking like normal idiomatic Go code. Adapting one of the examples from the proposal, if for example you wanted to desugar:In some cases, a programmatic transform that preserves behavior could end up with something like:
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 oferrin the same function, etc.The proposal talks about possible behavior differences between
ifandtrydue to named result parameters, but in that particular section it seems to be talking mainly about transitioning fromiftotry(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 fromtryback toifwhile 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.
@networkimprov
From https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md#efficiency-of-defer (my emphasis in bold)
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
Thanks, @owais, for bringing this up again - it’s a fair point (and the debugging issue has indeed been mentioned before).
trydoes leave the door open for extensions, such as a 2nd argument, which could be a handler function. But it is true that atryfunction doesn’t make debugging easier - one may have to rewrite the code a bit more than atry-catchortry-else.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
to
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
would be as simple as adding a catch statement at end followed by the code
Removing or temporarily disabling a handler would be as simple as breaking the line before catch and commenting it out.
Switching between
try()andif err != nilfeels 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 totry()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 ... catchwould 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 useif err != nilto add context is very similar to havingmake()vsnew()vs:=vsvar. 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 ... elseidea.//cc @brynbellomy
The more I think, I like the current proposal, as is.
If we need error handling, we always have the if statement.
@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>@jimmyfrasche
trycould be recognized as a terminating statement even if it is not a keyword - we already do that withpanic, there’s no extra special handling needed besides what we already do. But besides that point,tryis not a terminating statement, and trying to make it one artificially seems odd.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 termon, but I like the premise and overall look of the code structure.@yiyus https://github.com/golang/go/issues/32437#issuecomment-501139662
That can’t be done because Go1 already allows calling a
func foo() erroras justfoo(). Adding, errorto 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@magical
Having a handler is powerful, perhaps: I you already declared h,
you can
or
if its a function-like:
Then there was asuggetion to
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.
@ianlancetaylor
Agreed, and considering that an
elsewould only be syntactic sugar (with a weird syntax!), very likely used only rarely, I don’t care much about it. I’d still prefertryto be a statement though.@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 thattryis still needed in your suggestion, so I don’t see a reason for thereturn2- thetrycan just do thereturn. It would be more interesting if one could also writetrylocally, but that’s not possible for arbitrary signatures.@marwan-at-work, shouldn’t
try(err, "someFunc failed")betry(&err, "someFunc failed")in your example?I do not like this approach, because of:
try()function call interrupts code execution in the parent function.returnkeyword, but the code actually returns.@yiyus
catch, as I defined it doesn’t requireerrto be named on the function containing thecatch.In
catch err {, theerris what the error is named within thecatchblock. It’s like a function parameter name.With that, there’s no need for something like
fmt.HandleErrorfbecause you can just use the regularfmt.Errorf:which returns an error that prints as
foo: bar.Although I like the catch proposal by @jimmyfrasche, I would like to propose an alternative:
would be equivalent to:
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:
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.
Is the new keyword necessary in the above proposal? Why not:
We will have to agree to disagree here.
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…
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…)
You could avoid the goto labels forcing handlers to the end of the function by resurrecting the
handle/checkproposal in a simplified form. What if we used thehandle 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.As a bonus, this has a future path to letting handlers chain, as all existing uses would have a return.
@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
@josharian Thinking about the interaction with
panicis 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 ifsomeHTTPHandlerGutssucceeds and returnsnil. Thedeferruns in all exit cases, not just error or panic cases, so the code seems wrong even if there is no panic.@ianlancetaylor
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 leveragingpanic(). 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 implementtry(), just that they could if they really wanted to, and they had the features of generics.Does that clarify?
@mikeschinkel see my extension, he and i had similar ideas i just extended it with an optional block statement
@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.
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]andv, 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
CopyFileexample from the proposal along withdefer fmt.HandleErrorf(&err, "copy %s %s", src, dst), we get:@deanveloper, the
tryblock 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 asputting both statements within the same block is simiar to what we do for repeated
import,constandvarstatements.Now if you have, e.g.
This is equivalent to writing
Factoring out all of that in one try block makes it less busy.
Consider
If foo() doesn’t return an error value, this is equivalent to
Consider
Since returned error value is ignored, this is equivalent to just
Consider
Since returned error value is not ignored, this is equivalent to
As currently specified in the proposal.
And it also nicely declutters nested trys!
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 decisionI like the proposal but the fact you had to explicitly specify that
defer try(...)andgo try(...)are disallowed made me think something was not quite right… Orthogonality is a good design guide. On further reading and seeing things likeI wonder if may be
tryneeds to be a context! Consider:Here
foo()andbar()return two values, the second of which iserror. Try semantics only matter for calls within thetryblock where the returned error value is elided (no receiver) as opposed ignored (receiver is_). You can even handle some errors in betweenfooandbarcalls.Summary: a) the problem of disallowing
tryforgoanddeferdisappears 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.@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. 😃My initial reaction to this was a 👎 as I imagined that handling several error prone calls within a function would make the
defererror 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.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:
@natefinch if the
trybuiltin is namedchecklike in the original proposal, it would becheck(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:I like this proposal also.
And I have a request.
Like
make, can we allowtryto take a variable number of parametersas above.
a return error value is mandatory (as the last return parameter). MOST COMMON USAGE MODEL
as above, but if doPanic, then panic(err) instead of returning.
In this mode, a return error value is not necessary.
as above, but call fn(err) before returning.
In this mode, a return error value is not necessary.
This way, it is one builtin that can handle all use-cases, while still being explicit. Its advantages:
In the case decorating errors
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.
Is it possible to make it work without brackets?
I.e. something like:
a := try func(some)@griesemer
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.
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 twodefers 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.How would the tool know that I intended to handle the
errlater on in the function instead of returning early? Albeit rare, but code I have written nonetheless.@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
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:
should be:
@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.
@cpuguy83
The proposal presents an argument against this.
@bcmills @josharian Ah, of course, thanks. So it would have to be
Not so nice. Maybe
fmt.HandleErrorfshould implicitly pass the error value as the last argument after all.@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.
@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.
The unmodified value of
erris passed toHandleErrorf, and a pointer toerris passed. We check whethererrisnil(using the pointer). If not, we format the string, using the unmodified value oferr. Then we seterrto the formatted error value, using the pointer.@cpuguy83 My bad cpu guy. I didn’t mean it that way.
And I guess you gotta point that code that uses
trywill 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.
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:
or even better, like this:
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
attemptseems 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.
I think the
?would be a better fit thantry, and always having to chase thedeferfor error would also be tricky.This also closes the gates for having exceptions using
try/catchforever.@s4n-gt Thanks for this link. I was not aware of it.
@webermaster Only the last
errorresult is special for the expression passed totry, as described in the proposal doc.