Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A critique of go 2 error handling draft design proposal

go 2 has laid out the problem of error handling (Please read first). A wiki page was suggested for feedback. The feedback overwhelmingly suggested ways to not use scoped handlers, essentially creating alternative proposals

It is amazing to see the error handling problem being properly addressed. The existing proposal is good, but I think we can iterate to make it better. I think we can simplify it and address these issues:

  • Defining handlers as a stack is presumptious: it forces handlers to be side-effecting, and ruins interop with defer.
  • return statements are placed in handlers, ruining composition.
  • using check as a prefix function reduces code clarity.
  • a new anonymous function syntax is introduced into the proposal, but only available to handle.
  • the proposal introduces two keywords, but one may suffice

Ruby's blocks with early returns

Lets look at Ruby's block syntax.

irb(main):001:0> def block(&block); block.call() ; end
=> :block
irb(main):008:0> def return1(); block { return 1 } ; return 9 ;  end
=> :return1
irb(main):009:0> return1()
=> 1

Its even possible to write the returning function ahead of time

irb(main):006:0> def returns2() ; b = Proc.new { return 2 } ; block(&b) ; 3 end
=> :returns2
irb(main):007:0> returns2()
=> 2

Could we introduce Ruby's block's to go to solve this problem? In the simple case yes, but in the general case there is a composability problem: you cannot really compose two handlers that both want to return early.

irb(main):012:0> def notcomposed()
irb(main):013:1>   c = Proc.new { return 10 } ; d = Proc.new { return 20 } ;
irb(main):014:1*   c.call() + d.call()
irb(main):015:1> end
=> :notcomposed
irb(main):016:0> notcomposed()
=> 10

Existing proposal's handlers with returns

The current proposal has a handle keyword with a new special anonymous function that behaves equivalent to Ruby's Proc.new. That is, a return returns from the outer function.

func process(user string, files chan string) (n int, err error) {
    handle err { return 0, fmt.Errorf("process: %v", err)  }      // handler A

Just as in Ruby, the early return means composition does not work. In go, it should be easy easy to automatically handle the return: return the error given at the check statement, and any other return values should be set to the zero value. In fact, the proposal provides a special "Default Handler" which does just that.

The reason why the existing proposal has non-local returns is because in the proposal we are forced to construct handlers as a stack (see next section).

Stack scoping

The proposal forces handlers to compose as a stack scope. This is convenient in many cases. However, it forces a side-effecting style to handlers. Additionally, it limits the programmer and adds mental overhead to track the implicit stack. The stack-style itself is not always desired. That is, once a handler is added, it can only be deleted by adding a return to a new handler.

Lets look at the example from the proposal:

func SortContents(w io.Writer, files []string) error {
    handle err {
	return fmt.Errorf("process: %v", err)             // handler A
    }

    lines := []strings{}
    for _, file := range files {
	handle err {
	    return fmt.Errorf("read %s: %v ", file, err)  // handler B
	}
	scan := bufio.NewScanner(check os.Open(file))     // check runs B on error
	for scan.Scan() {
	    lines = append(lines, scan.Text())
	}
	check scan.Err()                                  // check runs B on error
    }
    sort.Strings(lines)
    for _, line := range lines {
	check io.WriteString(w, line)                     // check runs A on error
    }
}

Here handler B is able to disregard handler A by using a return. However, I think this demonstrates that although stacks may often be convenient, they are not quite what we want.

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