The try proposal separates error propagation from error context
Go's proposed try built-in shortens a conditional return while leaving error context to defer or explicit checks. Its restrictions expose the design choices behind that separation.
Repeated error checks are easy to shorten on paper. Preserving the meaning of the surrounding function is harder. Go's proposed try built-in makes that difficulty visible in the difference between assigning a function's results and returning early before the assignment happens. Even a small control-flow shortcut needs precise rules for the values it leaves behind.
Robert Griesemer's June 26 update on Go 2 places proposal #32437 among the changes selected for evaluation for Go 1.14. The design document, last updated June 12, specifies the behavior. This is a proposal under discussion, and its examples describe proposed Go syntax semantics, not a feature available in the released toolchain.
The June update asks for evidence about practical problems and useful applications. It describes a planned experimental implementation phase followed by a decision at the end of the development cycle. Selection for that process therefore does not establish that try will become part of a release.
I like Go's explicit errors because they make operational choices visible in ordinary code. Repetition can still become distracting when each check makes exactly the same choice. The interesting question in this proposal is how much repetition can disappear before the reader has to search elsewhere to understand failure behavior.
What try would return
The proposed built-in accepts an expression whose final result has type error. When that result is nil, try yields the preceding values. When it is non-nil, try assigns that error to the enclosing function's final error result and returns from that enclosing function.
The enclosing function must itself have a final result of type error. A use in a function without that result would fail at compile time. The rule gives try one behavior rather than choosing between an early return and a panic according to the surrounding signature.
For example, this is proposed code, assuming the ordinary strconv import.
func sum(a, b string) (int, error) {
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
return x + y, nil
}
On success, each call yields the parsed integer. If parsing fails, the enclosing function returns the error and the zero value for its unnamed integer result. The proposal requires no separate handler declaration for this case because the only policy is propagation.
A call whose sole result is an error can also be passed to try. In that case success produces no result. The common property is still the final argument value's exact error type. The proposal does not merely require a value assignable to the interface, partly to avoid the familiar difficulty of concrete nil pointers becoming non-nil interface values.
Assignments do not happen on failure
The proposed assignment semantics deserve more attention than the reduced line count. Suppose the enclosing function has named results a, b and err. Existing code can assign all the results of another function to those names and then check err. Even when the error is non-nil, the values assigned to a and b remain observable as return values.
Replacing that code with a, b = try(f()) changes the failure case. A failing try returns from the enclosing function before the assignment takes place. The named non-error results retain their earlier values. The design document explicitly describes this difference and advises preserving the ordinary if statement when its semantics are required.
That restriction follows the function-call-like shape of try. Its successful results become available to the surrounding assignment only if it returns to the call site. Failure instead takes a different control-flow path. A mechanical conversion that considers only the repeated error check could consequently change a function's observable results.
This is particularly relevant to functions that return partial progress alongside an error. The proposal does not require all such functions to use try, and it does not claim to replace every error test. Its benefit is limited to checks that really mean to propagate an error using the specified result behavior.
Error context stays in existing constructs
The earlier error-handling draft introduced check and handle. The new design drops a dedicated handler construct and proposes using defer when several failures need common context. A deferred closure can inspect and update a named error result before the function returns.
For example, an ordinary deferred wrapper can add the input value to a parsing error. The following code uses existing Go constructs, independently of whether try is adopted.
func parseCount(text string) (n int, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("parse count %q: %v", text, err)
}
}()
return strconv.Atoi(text)
}
The named result gives the closure access to the error being returned. In this example the wrapper formats context with %v; it makes no claim about preserving a chain of inspectable error values. The proposal also discusses possible helper functions for such wrapping, but those helpers are suggestions rather than existing standard library functions.
Using defer reduces the number of new language rules. It also moves the common context operation away from individual failure sites. A reader would need to connect the proposed try call to the deferred code at the function boundary. Whether that arrangement improves a particular function depends on how consistent the failures' handling really is.
I find the strongest case in functions whose failures share a clear context. Different recovery decisions still deserve separate checks near the operations that require them. A short propagation expression cannot decide whether an error should trigger a retry, translate into another result or be accepted as an expected condition.
Why the design uses a built-in
The proposal chooses ordinary function-call notation to avoid adding a keyword or operator. A keyword could conflict with existing identifiers. A built-in can be shadowed by an existing identifier without changing the language's expression grammar. That choice supports the stated requirement for backward compatibility.
Despite its spelling, try could not be an ordinary helper function. It needs to return from its enclosing function, and its successful result count and types depend on its argument. The document explains that adding generic functions alone would not provide those control-flow semantics.
Its restrictions also prevent misleading function-like uses. The design disallows using try as the called function in a go statement, since there would be no suitable enclosing function to return from in the new goroutine. It also disallows a deferred try, where delayed execution could overwrite an error already being returned. Like other built-ins discussed in the document, try cannot be assigned as an ordinary function value.
Those rules are part of evaluating the proposal's complexity. The notation is familiar, but its control flow is special. A reader needs to recognize a particular built-in call as a possible return point, with the named-result and deferred-function behavior that entails.
The document's account of earlier iterations explains how the restrictions developed. One version accepted an optional handler function, raising questions about a handler that returned nil and whether that should cancel an error or cause a successful return. Another version panicked in functions without an error result. Adding an error result to such a function could then silently change the behavior of its existing checks.
The current design removes that context-dependent panic behavior. It also removes the optional handler parameter, leaving common error augmentation to deferred code. These choices reduce the number of interactions to specify, but require some functions to keep explicit checks. That is a deliberate limit on the situations the proposed built-in can express.
What remains to evaluate
The design discusses using try in tests but proposes no specific change to the testing package. A conventional test function does not return an error, so the current proposal's signature restriction prevents direct use there. Suggested error-returning test functions raise further questions about reporting the failing line and handling subtests.
Performance is another stated concern because the context strategy relies on defer. The document describes hoped-for improvements and ongoing work. Those expectations should not be presented as measured results for this proposed feature. The evaluation needs to distinguish a language rule from the performance of an implementation that has not yet completed the proposed process.
The June update also separates error handling from other language work. Generics remain under development without a concrete proposal at this point, while smaller changes include overlapping embedded interfaces and a proposed vet diagnostic for integer-to-string conversion. The common process asks for examples and implementation experience before a final decision.
For try, the most useful examples must preserve the original function's exact failure behavior. Named partial results and differing error policies are concrete places to compare the proposed code with existing checks. The proposal leaves those checks available when propagation through the final error result does not express the function's contract.