Description
In search for a solution to eliminate the infamous
if err != nil {
return err
}
3-liner, I came up with an intriguing idea: What if Go could let you mark any identifier on the left-hand side of an assignment or variable declaration to immediately return the new value, when it is != nil? While investigating this idea further, I eventually decided to cut it down to error handling as of now. You can read about the much broader approach in the section Where I’m coming from with this proposal – and where this could lead to, in case you are interested.
But let’s dive right into how this proposal could improve error handling in Go.
Use ^ to mark error identifiers or error handling functions
The design proposes a change to the syntax of Go to allow the ^ character as a prefix to an identifier on the left-hand side of
- variable declarations with initializers
- short variable declarations
- = assignments (no prefix operators allowed)
and only inside a function body.
Example of marking a returned value of type error in a short variable declaration:
^err := failableTask()
When used in this context, the ^ character may be called shortcut return mark for now (see section Considerations regarding the naming of the feature for alternative names).
^-marked identifiers return if != nil
To put it in a formalized way: When the newly assigned value is != nil, the identifier’s new value will be returned.
As an example, this code
f, ^err := os.Open(filename)
is a shorter version of
f, err := os.Open(filename)
if err != nil {
return err
}
Using function identifiers to handle errors
When an error should not only be returned as it is, but needs to be wrapped or treated otherwise, the ^-mark should be allowed on a function value like in the following example:
func doWork() error {
handleError := func(err error) SpecificError {
return SpecificError{ ..., Err: err }
}
...
f, ^handleError := os.Open(filename)
...
}
The function behind handleError
gets called, when os.Open
returns an error (meaning it is != nil). The error is passed as a parameter to the handler function, which returns a SpecificError
in the example and that type implements the error interface. It is thus compatible with the return value of function doWork and supplies the value to be returned, an instance of SpecificError
.
More formally, a function value reference can be preceded by the ^-mark and turned into an error handler, if
- The function has exactly one parameter
- The sole parameter’s type implements the error interface
- The function returns an error type that is compatible with the last return value of the function doing the actual assignment to the error handler function using the ^-mark
What is returned for other return values of a function (anything but the error)?
If a function is being shortcut-returned by a ^-marked error or handler function, any return value other than the error should return its type’s zero-value. This matches the behavior of other proposals.
I think the exception should be named return values that already had a (non-zero) value assigned, that is: They should keep and return their already assigned value. But I put it up for debate what would be more of a gotcha: the implicit zeroing of an explicitly set return value or that a return value of a "failable" function is != it’s zero value when an error occurred. A handler function could always explicitly zero any named return value, though.
Allow an error handler function to return more than just the error
Following up on then previous section, it should be permitted for an error handling function to return not only the error to the function that uses the error handler, but all of its return values. That would e.g. allow handlers to supply fallback values when a certain type of error occurs. Example:
func printCurrentDirectoryCmd() string, error {
handlePwdErr := func(err error) string, error {
handleCdErr := func(err error) error {
return errors.New("Unknown OS environment")
}
cmd := exec.Command("cd")
^handleCdErr := cmd.Run()
return "cd", nil
}
cmd := exec.Command("pwd")
^handlePwdErr := cmd.Run()
return "pwd", nil
}
The function printCurrentDirectoryCmd
should return the name of the command to print the current working directory: pwd in Unix-like environments, cd in Windows. First, pwd is tried. If the command is present, the function returns the name of the command and no error in the last line of the function body.
When pwd cannot be found and an error occurred, the error handling function handlePwdErr
is called. Look at its return values: it returns the same type as printCurrentDirectoryCmd
and in the same order. The statement ^handlePwdErr := cmd.Run()
thus returns both values from handlePwdErr
, which tries to do the same thing for the cd command. If cd isn’t found either, an error is returned by handleCdErr
, along with an empty string (the zero-value of type string).
Let an error handler function return a type other than an error
Let’s look at the example from the previous section again. Function printCurrentDirectoryCmd
could as well just omit the error and state in its documentation that the returned command is empty, if no matching command could be found for the current execution environment. An error handling function should be allowed to return a type other than error, so the example could be rewritten like this:
func printCurrentDirectoryCmd() string {
handlePwdErr := func(err error) string {
handleCdErr := func(err error) string {
return ""
}
cmd := exec.Command("cd")
^handleCdErr := cmd.Run()
return "cd"
}
cmd := exec.Command("pwd")
^handlePwdErr := cmd.Run()
return "pwd"
}
Semantics of ^-mark syntax
A return statement passes control (along with the return values) up the call stack to its callee. The same is true for a shortcut return. The ^ character is occasionally used to point to something above in various contexts, because it looks like the head of an arrow pointing upwards.
func main() {
^ // ...
| func doWork() error { // call via doWork(), function body for illustrating program flow
| // ...
|__________
|
f, ^err := os.Open(filename) // pass err up the call stack to main
I could imagine that, in everyday talk, Gophers would say things like “just up-return the error” or “just up the err”, which would make perfect sense to me.
Considerations regarding the naming of the feature
I want to put the following four alternative names for this feature up for discussion:
- shortcut return [mark] (my favorite)
- conditional up-return [mark], could be abbreviated as coup [mark]
- quick return [mark]
- return on assignment [mark]
Where I’m coming from with this proposal – and where this could lead to
As I mentioned before, in search for a solution to get rid of error handling boilerplate code I imagined an approach that would go way beyond handling errors. The idea would be that you could mark any identifier on the left-hand side of a (short) variable declaration or assignment using the ^ character to immediately return its value when the new value is unequal to the zero value of the type. How could this be useful? Let’s say you have multiple functions that wrap certain command line program calls and pass back their return codes of type int. By convention, 0 means that the command line program executed successfully. Calling such a series of command line programs - and aborting when one of them fails - could be written very elegantly using the ^-mark just like this:
^rc := cmd1()
^rc := cmd2()
^rc := cmd3()
I eventually discarded this idea for now, because it raised more and more questions, the longer I thought about it. And because I wasn’t sure what the implications of this language feature being widely available would have on Go code. Most importantly, how this would blur the lines between assignment, function calls and conditional program flow.
The good thing is: Allowing the ^-mark on any type, not just errors, could be brought to Go at a later point in time without the need for a breaking change.
Restricting the identifier name when shortcut-returning plain errors
It should be considered to allow only err
as a valid identifier name when marking plain errors with the ^ character. Otherwise, small typos could lead to possibly hard to debug and identify errors like in the following example:
func doWork() error {
handleError := func(err error) SpecificError {
return SpecificError{ ..., Err: err }
}
...
f, ^handelError := os.Open(filename)
...
}
Can you spot the typing error? In this case, handleError
isn’t being called and the call to os.Open
returns its error as-is, which is unintended behavior.
To prevent these kinds of programming mistakes, the first incarnation of the feature should allow the shortcut-retuning of plain-errors with ^err
only. This rule could be relaxed later, either when extending shortcut returns like described previously or when a general feeling develops in the Go community that this restriction isn’t necessary or somehow prevents better code.
Resuming control
Other proposals like the one by Marcel van Lohuizen have no way of resuming control, once the error handling is underway. This proposal would have two ways of providing conditional shortcut-returning on errors which typically means inspecting them more deeply. The first approach is based on syntax introduced so far, and could be written in a 2-liner like this:
err := failableTask()
^err = handleErr(err)
So, the first statement would simply capture the error. In the second statement, a handler function would be called, that conditionally checks the supplied error and returns an error or not, based on what is appropriate. Because the err on the left-hand side of the assignment is ^-marked, the statement would shortcut-return that error in the surrounding function if handleErr
returns a value != nil.
For the second idea, let’s look again at what the meaning of the ^-mark is: When used with a handler function, that function is called only when the error parameter is != nil. And errors are returned directly, when != nil. To combine and nest these two steps, a double ^-mark could be considered:
^^handleErr := failableTask()
How to read this? Again, this is a nested statement, so the first thing happening is ^handleErr
, meaning that the handler function is only being called if the sole parameter is != nil. If the handler is called, it’s return value (an error) is evaluated a second time by the first of the two ^ characters in the above statement: It shortcut-returns the error returned by handleErr
from the surrounding function.
To recap:
Benefits
- no new reserved word(s) needed, unlike in most other solutions on error checking and handling
- syntax change should be backwards compatible
- ^-mark introduces minimal visual clutter, while still having enough eye-catching ability
- understanding of code is always clear on-line: errors to return are marked or handler function being used must be mentioned by name => no subordinate "magic" happening
Downsides
- blurs the lines between assignment, function calls and conditional program flow
- editors/tools need to recognize and handle the syntax change accordingly
- using regular functions for (error) handling instead of a special language construct leads to otherwise unnecessary copying of function parameters