Skip to content

Instantly share code, notes, and snippets.

@fragglet
Last active September 13, 2018 09:46
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 fragglet/df6c5471771d87b2ad597d2efc57cb3e to your computer and use it in GitHub Desktop.
Save fragglet/df6c5471771d87b2ad597d2efc57cb3e to your computer and use it in GitHub Desktop.
Go 2 errors response: One Handler Per Function

This is a response to the draft design for Go 2 error handling.

I think that the main thing I dislike about the proposed check/handle blocks is that they feel too much like a kind of "magic goto" that breaks the normal program flow. I worry that it won't be straightforward to follow the logic of what happens when a check fails. In the simple handle err { return err; } it isn't so bad but the examples with chained handle blocks seem quite counterintuitive.

I'm in agreement with Alessandro Arzilli in that I think chaining is the main source of my objection. It seems to me that the burden of proof ought to be to show that the chaining mechanism is necessary. Certainly single-handle case is the overwhelmingly most common case; how common is the more complicated one that uses multiple handlers? Examining existing code should make it possible to determine whether the extra complication is really necessary (a simple metric might be: how many if err != nil blocks contain statements other than a return?)

Alessandro proposes an alternative design where one handler is allowed per block, I'd go further and suggest it should be a limit of one handler per function. My rationale is:

  • It clearly satisfies the common case of "just want to pass back an error"
  • It aids readability since it will always be trivial to see the handler that will be invoked in response to a failed check.
  • Without chaining, handle blocks must (?) always end in a return, which links them to the function scope rather than the block scope.

Note that single-handler is still enough to support the cases that require chaining; they simply require splitting code across multiple functions. For example the CopyFile example looks like this:

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

	r := check os.Open(src)
	defer r.Close()

	return check performCopy(r, check os.Create(dst), dst)
}

func performCopy(r, w *os.File, dst string) error {
	handle err {
		w.Close()
		os.Remove(dst)
		return err
	}
	check io.Copy(w, r)
	check w.Close()
	return nil
}

The main ugliness seen here is the need to pass the dst parameter to the second function, but it isn't too bad.

@AndrewWPhillips
Copy link

I agree that having chain of handlers is too confusing but then having a single handler per function makes it very much like defer.

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