The argument over try
It has been six days since the try proposal was posted and the issue already has hundreds of comments.
Robert Griesemer has posted a useful summary of the discussion. After reading far too much of the thread, I think the arguments are clearer now. They are also better than “I hate typing if err != nil” versus “real programmers enjoy typing it,” which is where these discussions usually go to die.
The strongest case for try is ordinary sequential code:
func readConfig(name string) (*Config, error) {
b := try(ioutil.ReadFile(name))
var cfg Config
try(json.Unmarshal(b, &cfg))
return &cfg, nil
}
There is very little error handling in the expanded version. Both errors are simply returned. The extra if statements interrupt the description of what the function does without adding a decision.
This is not merely speculation. Some people have tried converting code from the standard library. One conclusion was that try made most of those examples easier to read. Another person looked at much the same evidence and reached almost exactly the opposite conclusion.
The proposal has other good properties. It does not introduce exceptions. Errors remain ordinary return values. Existing code keeps working. try handles the dull case while an if remains available whenever the error needs local attention. I find that separation appealing.
The invisible return
The best argument against it is simple: try looks like a function call but changes the control flow of its caller.
Consider this:
req.Header = Header(try(tp.ReadMIMEHeader()))
That assignment can return from the enclosing function. A struct literal containing try can return. A nested expression can contain several possible returns on the same source line. Today my eyes know that constructing a value does not quietly leave the function. With try, they would have to search inside every expression.
Some commenters therefore prefer a keyword or statement form:
try f := os.Open(name)
It is harder to miss, but it is also a larger language change and it gives up the proposal’s useful ability to fit where an expression fits. Restricting try may make it safer while removing much of what makes it attractive. Language design is fun like that.
There are practical problems too. If I add a debugging print to one particular failure today, there is an obvious block in which to put it. If several operations use try and one deferred handler wraps all of their errors, identifying the exact operation may require changing the code back to an if.
Coverage has a similar problem. The explicit error block is visible to coverage tools. A hidden return inside try creates a branch that source-level coverage needs to represent somehow. Debuggers and stack traces also need enough information to distinguish multiple try calls on one line. These are practical objections, not just a matter of taste.
The context problem
I am less convinced by the proposed use of defer for error context than I am by try itself.
A deferred handler can add function-level context once:
defer fmt.HandleErrorf(&err, "reading configuration %s", name)
That is lovely when every failure in the function deserves the same context. It is less useful when opening the file, parsing it, and validating it each need different information. The handler also runs on every return and encourages named error results. None of this is fatal, but it means the proposal is strongest exactly where error forwarding is least interesting.
So I still like try, with qualifications large enough to require their own parking space. I would avoid nesting it and use one operation per line. I would keep if err != nil wherever an error needs local context, cleanup, logging, or translation. In that style the hidden control flow is bounded and the useful code becomes much easier to see.
But Go cannot add a feature on the assumption that everybody will use my preferred style. If nested try is legal, nested try will exist. If a return can hide in a composite literal, people will write code that does exactly that.
I thought the original proposal was likely to fail because it felt unlike Go. The discussion has produced better reasons than feelings. I still think we may be rejecting an idea that would become ordinary and useful with time, but it is no longer difficult to understand why so many people are wary of it.