PLEASE NOTE: I am not a language designer or have any experience in implementing or mainting a language or compiler. So, I would love to write out the EBNF syntax for what I am about to propose, but alas, I wouldn't know where to start. So, please forgive the informal nature of the proposal and feel free to ask questions; hopefully I'll be able to keep up with actual experts in this field and if not I apologize for my naivety in advance!
When the try()
proposal was closed, I was relieved. There is a problem to be solved here, but try
, to me at least, created more problems while trying to solve one.
The main problems, I think it created were the following:
-
try
reduced the visiblility of failures. For example:info := try(try(os.File(fileName)).Stat())
There are 2 possible failures packed in this single line, but to my eyes, they are harder to see. This is one of the reasons I actually prefer
if err != nil
, because it forces each operation and it's failures to be addressed separately. Thus, maintaining the visibility of errors is an attribute of an error-handling syntax that I would like to keep. -
try
reduced the visibility of function exits. Refering to the same example in the previous point, bothtry
invokations can cause a function to stop executing, yet my eyes haven't been trained to interprettry
as a function exit. While I might be able to get used to it, it seems harmful to new Gophers;try
for other languages generally is a prefix to a block of code to be attempted and has no flow-control connotations (catch
is usually a flow-control keyword, whereastry
is kind of, for lack of a better term, a "pass-through" keyword)
What I (very informally) propose is a syntax which uses a new catch
keyword to denote a block of code that handles failures in function calls, like so:
package main
import (
"strconv"
"github.com/pkg/errors"
)
func add(x, y string) (int64, error) {
start := time.Now()
xn := strconv.ParseInt(x, 10, 64) catch (err error) {
return 0, errors.Wrap(err, "x arg is not an integer")
}
yn := strconv.ParseInt(y, 10, 64) catch (err error) {
return 0, errors.Wrap(err, "y arg is not an integer")
}
return xn + xy, nil
}
func _main() error {
u := add("3", "1") catch (err error) {
return errors.Wrap(err, "failed to add")
}
println("result: " + strconv.Itoa(int(u)))
}
func main() {
_main() catch (err error) {
println("ERROR: " + err.Error())
}
}
The catch
syntax as demonstrated in the above example, to my eyes, clearly displays the flow of execution in cases of error and no error. It also avoids creating the problems that try
does, by mainting the visibility of failures and not being in control of function exit.
Arguments could be made that it doesn't reduce much boilerplate over this:
if xn, err := strconv.ParseInt(x, 10, 64); err != nil {
return 0, errors.Wrap(err, "x arg is not an integer")
}
But, in this example, xn
gets scoped into the if
block, preventing it from being used in the whole function context, forcing the writer to do something awkward like define xn
and err
outside like this:
var xn int64
var err error
if xn, err = strconv.ParseInt(x, 10, 64); err != nil {
return 0, errors.Wrap(err, "x arg is not an integer")
}
In the catch
syntax, the non-error return values would be scoped correctly to the outside block, and err
would be scoped to the catch
block.
The general structure of the expression I had in mind was:
[non-error variable assignments] := [function call] "catch" "(" [variable identifier] [type] ")" {
// ... error handling code
}
**Why catch (err error)
instead of just catch err
? **
I have seen other proposals omit the parenthesis and type declaration and I find it to be far too minimal. In effect, the catch
block automatically introduces a new variable (err
) to the catch
block scope. Therefore, we should see some declaration that includes the type for readability and clarity. Also, mimicking the syntax of an argument list can smooth over learning curves that would be introduced by a different syntax; code readers would pick up on the argument list style easier in my opinion.
"This proposal does not reduce the overhead of handling errors nearly enough."
There is a lot of boilerplate involved with handling errors in Go, and after all of these proposals, I am beginning to believe that Go isn't creating more unnessacary code to write to handle errors; instead, other languages don't do enough to reveal just how many operations can cause failures.
Therefore, any proposal that attempts to discard any more than structure for handling errors than this proposal, will come at the cost of failure visibility in my opinion. I would love to be proven wrong on this point, but this is my current belief.