Skip to content

Instantly share code, notes, and snippets.

@nkcmr
Last active July 17, 2019 14:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nkcmr/90d6567f219cd0df531867fdc2341fce to your computer and use it in GitHub Desktop.
Save nkcmr/90d6567f219cd0df531867fdc2341fce to your computer and use it in GitHub Desktop.
Informal Golang Proposal: `catch` block as an alternative to `if err != nil`

Informal Golang Proposal: catch block as an alternative to if err != nil

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, both try invokations can cause a function to stop executing, yet my eyes haven't been trained to interpret try 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, whereas try 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment