Skip to content

Instantly share code, notes, and snippets.

@gregwebs
Last active October 3, 2018 20:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gregwebs/02479eeef8082cd199d9e6461cd1dab3 to your computer and use it in GitHub Desktop.
Save gregwebs/02479eeef8082cd199d9e6461cd1dab3 to your computer and use it in GitHub Desktop.
Go 2 error handling alternative: handlers as normal functions and an error return?

Its 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

It's useful to take a step back and clearly state what we are trying to do:

  • provide an abstraction that allows for the insertion of a return statement for errors.
  • compose handler functions together before they are used with the error return

In the existing go language, I cannot write a handler function which will create an early return from the function. There are a few approaches that languages use for this control flow:

  • Macros (e.g. Rust originally used a try! macro).
  • Ruby supports anonymous functions that return from the enclosing function (method)
  • exceptions or gotos
  • A Haskell function can use a Monad that supports early return

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.

Alternative Proposal: handlers as functions, just a special check

Lets look at our goals again:

  • provide an abstraction that allows for the insertion of a return statement for errors.
  • compose handler functions together before they are used with the error return

Composition can be handled with normal functions. That means we just need a mechanism to insert a return. For my proposal, I will use a question mark operator ? rather than a check keyword. This is because the operator can be used postfix and there are advantages in readability to a postfix positioning (see Appendix: Operator versus check function). return is handled solely by the ? operator. Any return in a handler function is just the normal function return.

Putting this together, lets re-write the above SortContents

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

    lines := []strings{}
    for _, file := range files {
	handlerFiles := func(err error) error {
	    return fmt.Errorf("read %s: %v ", file, err)
	}
	scan := bufio.NewScanner(os.Open(file) ? handlerFiles)
	for scan.Scan() {
	    lines = append(lines, scan.Text())
	}
	scan.Err() ? handlerFiles
    }
    sort.Strings(lines)
    for _, line := range lines {
	io.WriteString(w, line) ? handlerAny
    }
}

In this new form we no longer need to fight the stack with a non-local return in our handler.

Let's re-write this example from the proposal (slightly simplified):

func process(user string, files chan string) (n int, err error) {
    handle err { fmt.Errorf("process: %v", err)  }      // handler A
    for i := 0; i < 3; i++ {
	handle err { err = fmt.Errorf("attempt %d: %v", i, err) } // handler B
	check do(something())  // check 1: handler chain B, A
    }
    check do(somethingElse())  // check 2: handler chain A
}

Alternative:

func process(user string, files chan string) (n int, err error) {
    ahandler := func(err error) error { return fmt.Errorf("process: %v", err) }
    for i := 0; i < 3; i++ {
	bhandler := func(err error) error { return fmt.Errorf("attempt %d: %v", i, err) }
	do(something()) ? ahandler.ThenErr(bhandler)
    }
    do(somethingElse()) ? ahandler
}

Note that there is no special handle keyword, just a special check done by the ? operator. It is possible to combine handlers in the same way one would combine functions:

do(something()) ? ahandler.ThenErr(func(err error) error {
	return fmt.Errorf("attempt %d: %v", i, err) }
)

Or

do(something()) ? func(err error) { return ahandler(bhandler(err)) }

The example uses a hypothentical .ThenErr method (see appendix) as a way to compose error functions.

Checking error returns from deferred calls

The original proposal states that check is incompatible with defer. This is due to its use of a handler stack. So this alternative proposal has no such problem. It is quite possible now to support returning errors from defer:

defer w.Close()?

Results

This alternative proposal avoids introducing a special handle construct, and gives much greater flexibility to the programmer. Handlers can be naturally composed as functions, rather than fighting each-other with side-effects. By avoiding implicit stacking, it could return errors from defer. The code is still much more succinct and organized than current go error handling code.

This alternative proposal results in code that is slightly more verbose than the original proposal (see next section).

Handle and anonymous function syntax

The main reason this alternative proposal is more verbose than the original is that the handle construct is not just a keyword. It also introduces a special anonymous function syntax that is lighter-weight and infers types.

handle err { return fmt.Errorf("process: %v", err) }

Without this syntax, the proposal would read:

handle func(err error) error { return fmt.Errorf("process: %v", err) }

I think it is worthwhile to explore having anonymous functions that are lighter-weight. However, I think this should be useable anywhere rather than just with a single keyword. For example, the Rust syntax version:

handler := |err| fmt.Errorf("process: %v", err)

But its also possible to try to extend the existing go syntax:

handler := func err fmt.Errorf("process: %v", err)

But please leave this for another proposal rather than throw it in the mix with error handlers!

The question mark operator: unary or binary.

Note that the question mark operator can be used as a unary to just return the exception without any handlers running.

something()?

This is equivalent to

something() ? func(err error) error { return err }

I am favoring writing the unary form without any spaces in this case (more similar to Rust), but we should use whatever syntax the community finds best.

Notes on error handler function types

To respond to errors we want to do one of two things:

  • cleanup (side-effecting): (error) -> nil or () -> nil
  • modify the error: (error) -> error

It is possible to do both in one function, but then your function signature is the same as modifying the error. The question mark operator should accept all forms. A cleanup function will automatically be converted to return the original error that would have been passed to it.

Note that helpers which compose error functions such as ThenErr also need to compose the cleanup function such that it returns the error given to the cleanup function.

Appendix: Handling errors within the handler itself

A cleanup handler may generate a new error that should be propagated in addition to the current error. I believe this should just be handled by a multi-error technique, e.g. multierr.

Appendix: custom error types

The existing proposal seems like it would cast a concrete error type to the error interface when it is passed to a handler. I don't think this proposal is fundamentally different. I think this issue should be solved by the generics proposal.

Appendix: defer-like statements and break/continue

It is possible to support right-hand-side statements

f() ? panic("oh no")

However, I don't thing the complexity is justfied. It may be useful, however, to think about supporting break/continue.

f() ? break

I think though, that in many of those cases one would want to set an error value before breaking, so I am not sure how useful this is in practice.

Appendix: ThenErr

An implementation of ThenErr (some more combinations are needed, this should show the concept)

type Cleanup func(error)
type CleanupNoThanks func()
type ModifyError func(error) error


func (fn1 CleanupNoThanks) ThenErr(fn2 ModifyError) {
	return func(err error) error {
		fn1()
		fn2(error)
	}
}

func (fn1 ModifyError) ThenErr(fn2 Cleanup) {
	return func(err error) error {
		newErr := fn1(error)
		fn2(newErr)
		return newErr
	}
}

Appendix: Operator versus check function

The original proposal had just one argument given to check. This alternative favors the question mark because there are now 2 arguments. The original proposal states that there is a large readability difference in these two variants:

check io.Copy(w, check newReader(foo))
io.Copy(w, newReader(foo)?)?

However, I think this is a matter of personal preference. Once there is a left-hand-side assignment, the readability opinion may also change.

copied := check io.Copy(w, check newReader(foo))
copied := io.Copy(w, newReader(foo)?)?

Now lets add in a handlers and check our preference again.

copied := check(io.Copy(w, check(newReader(foo), ahandler), bhandler)
copied := io.Copy(w, newReader(foo) ? ahandler) ? bhandler

I believe ? will be slightly nicer to use due to

  • fewer parantheses
  • putting error handling solely on the right-hand-side rather than both the left and right.

Note that it is also possible to put all the error handling on the left-hand-side of the error source.

copied := check(bhandler, io.Copy(w, check(ahandler, newReader(foo)))

But because a success result is still transferred to the left, I prefer keeping error handling on the right-hand-side.

Here is another proposal that I believe advocates the same solution proposed in this alternative, but with a check function.

Appendix: invoke the handler from the assignment position

Other proposals are similar to this, but suggest invoking the handler on the left-hand-side in the assignment position.

v, #handler := f()

Where # is much like ?: it invokes a handler with an error. There has also been a suggestion to extend this to a more general pattern matching.

v, nil ? handler := f()

I would like to see a separate pattern matching proposal for go. In the mean-time, I still prefer error handling on the right-hand-side because it allows for inline handlers.

Appendix: built-in result type

A go programmer that has used Rust, Swift, Haskell, etc will be missing a real result type. I would like to see a go 2 proposal for discriminated unions which includes a result type. However, I think both the original proposal and this alternative proposal would work fine with the addition of a result type. This is because go effectively already has a result type. It is a tuple where the last member is of type error.

Appendix: intermediate bindings for readability

Error handling on the right-hand-side may increase line length undesirably or seem to be easy to miss. Its always possible to use an intermediate binding.

v, err := f(...) // could be a million lines long
err ? handler

Appendix: left-hand-side

Its possible to support placing the handler on the left-hand-side

v := handler ? f(...)

Appendix: all proposal examples re-written

Below are the rest of the code snippets shown in the original proposal, transformed to this alternative proposal.

func TestFoo(t *testing.T) {
	handlerFatal := func(err error) { t.Fatal(err) }
	for _, tc := range testCases {
		x := Foo(tc.a) ? handlerFatal
		y := Foo(tc.b) ? handlerFatal
		if x != y {
			t.Errorf("Foo(%v) != Foo(%v)", tc.a, tc.b)
		}
	}
}

func printSum(a, b string) error {
	handler := func(err error) error { fmt.Errorf("printSum(%q + %q): %v", a, b, err) }
	x := strconv.Atoi(a) ? handler
	y := strconv.Atoi(b) ? handler
	fmt.Println("result:", x + y)
	return nil
}

func printSum(a, b string) error {
	fmt.Println("result:", strconv.Atoi(x)? + strconv.Atoi(y)?)
	return nil
}

func CopyFile(src, dst string) error {
	handlerAll := func(err error) error {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	r := os.Open(src) ? handler
	defer r.Close()

	w := os.Create(dst) ? handlerAll
	handleCleanup := handlerAll.ThenErr(func(err error) {
		w.Close()
		os.Remove(dst) // (only if a check fails)
	})

	check io.Copy(w, r) ? handlerCleanup
	check w.Close() ? handlerCleanup
	return nil
}


func main() {
	handlerAll := func(err error) error {
		log.Fatal(err)
	}

	hex := check ioutil.ReadAll(os.Stdin) ? handlerAll
	data := check parseHexdump(string(hex)) ? handlerAll
	os.Stdout.Write(data)
}
@bhinners
Copy link

bhinners commented Sep 27, 2018

I like a lot about this proposal, especially:

  1. Handlers as functions seems both more general and more like idiomatic Go, and also eliminates the need for one new keyword.
  2. Explicit chaining of handlers with something like ThenErr is both more general and clearer.
  3. Using ? on the RHS keeps the primary logic on the left making it easier to scan code for its intent. It's also succinct.

Quick question/suggestion: maybe

    something()?

should follow the behavior described by @the-gigi: return the zero value of unnamed return values and the current value of named return values?

I like the idea of including support for ? break and ? continue, too.

I'm not a fan of putting the handler invocation on the LHS of an assignment as some other proposals and some of the discussion above suggests, primarily because it's simply not an assignment. It feels unfamiliar and misleading to me.

I also like your proposal for streamlining the syntax for anonymous functions, which complements this proposal, but - as you say - should be considered separately.

PS: Your last two rewritten examples used the check keyword. Was that a mistake, or did I misunderstand something?

@bhinners
Copy link

bhinners commented Oct 3, 2018

Instead of

do(something()) ? ahandler.ThenErr(bhandler)

why not

do(something()) ? ahandler ? bhandler

The latter syntax, in addition to being more concise and consistent, implies that bhandler only gets called if ahandler returns a non-nil error value.

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