Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Simple Error Handling for Go 2

Key Parts of Error Handling

val, err := function()
if err != nil {
	//handler codes...
}

Error handling consists of 3 key parts, trigger, handler and binding rule. In Go 1, trigger is if err != nil, handler is a piece of code in {}, and binding rule is obvious. Handler holds the control flow switch.

Actually, I donot think error handling in Go 1 has any functional problem. The only problem is repetition, especially repetition of triggers. Go 1 allows us to place trigger and assignment in one line, but that does not really solve the problem.

if val, err := function(); err != nil {
	//handler codes...
}

Trigger

Triggers are the same, so it's a good idea to represent the trigger as one word. With more parameters, trigger can becomme more powerful. Trigger with 1 parameter can do nothing more than detect error. Trigger with 2 parameters can specify handler. Trigger with more than 2 parameters can specify a handler chain, or deliver extra parameters to the handler.

check(function, handler1, handler2, handler3)
func handler(path string, err error) error { return fmt.Errorf("%s: %v", path, err); }
check(function, handler, path)

Handler

Handlers are not always the same. What should we do is to make common handlers sharable, leaving special ones unchanged. Traditionaly, we create function for sharing code. The most common error handler, which just pass error to upstream, can be compiler-predefined. Implements of an interface may share one error handler. Main drawback of using function as error handler is function cannot change the control flow of its caller directly.

Binding Rule

A simple clear binding rule may be important than trigger and handler.

for (i := 0; i < 2; i++) {
	handle err {...}	//A
	check function()	//Whould B be invoted?
	handle err {...}	//B
}

Complicated binding rule looks powerful, but help little in fact. When we have to think hard to figure out the consequence of error hanling, error hanling become poison.


Original Feadback

Error handling should act as an easy exit in most cases. A complicated mechanism looks like defer/recover but works differently may be confusing. I suggest a simple way for error handling without new keywords.

func getDivisorFromDB(key string) (uint, error) {
	//...
}

func GetDivisor(key string) (uint, error) {
	exit := func(err error) (uint, error) {
		return 1, fmt.Errorf("fail to get divisor with key \"%s\": %v", key, err)
	}

	divisor := check(getDivisorFromDB(key), exit)

	//...
	return divisor, nil
}

In code above, "check" is a builtin function like "make", accepting 1 or 2 parameters. The first is the function to be checked, the second is the error handler. An error handler is a function having the same output type with the function to be checked. If the error handler is not specified, a default one will be created to pass through error.

divisor := check(getDivisorFromDB(key), exit)  //divisor := check(getDivisorFromDB(key))

is equal to

divisor, err := getDivisorFromDB(key)
if err != nil {
	return exit(err)  //return err
}

Chained error handling is not the common case, because the defer mechanism will handle resouce release. We just want a quick exit when error occurs. If someone really needs chained handling, he can do it explictly in the exit function.


This does not cover all cases of error handling, but improves exiting on error which contributes the most repeated code. Uncommon dedicate operations should be done with old style. If many breaks or continues are need in a loop, that fat loop body should be refactored into one or more functions where the check&exit works.

@pborman

This comment has been minimized.

Copy link

@pborman pborman commented Aug 30, 2018

Unless exit is going to be used for more than one check, it is more clear to say:

divisor, err := getDivisorFromDB(key)
if err != nil {
	 return 1, fmt.Errorf("fail to get divisor with key \"%s\": %v", key, err)
}

I think the most common exit will be the equivalent of the default handle. I don't think there needs to be a handler at all, other than defer with a named return.

@PeterRK

This comment has been minimized.

Copy link
Owner Author

@PeterRK PeterRK commented Aug 30, 2018

Usually, we don't need any custom exit function.

divisor := check(getDivisorFromDB(key))
@8lall0

This comment has been minimized.

Copy link

@8lall0 8lall0 commented Aug 30, 2018

I don't see how this is different from the original design, except for the keywords.

@PeterRK

This comment has been minimized.

Copy link
Owner Author

@PeterRK PeterRK commented Sep 2, 2018

Error handling in Go 1 is good except its ugly code style. We need to update its look, not its action.

@nicl

This comment has been minimized.

Copy link

@nicl nicl commented Sep 5, 2018

With generics this solution becomes possible without introducing a new kind of control flow (which check/handle does), so seems somewhat nicer.

@deanveloper

This comment has been minimized.

Copy link

@deanveloper deanveloper commented Sep 8, 2018

Error handling in Go 1 is good except its ugly code style. We need to update its look, not its action.

I completely disagree with this.

The Go 1 way of error checking is extremely clear as to what it does. It's also very readable, what's wrong with it is the repitition, not the look of it.

Also, I don't like that there's no explicit return for the function, especially when you omit the "exit function"

(sorry for all the edits, I'm on mobile and keep accidentally submitting early)

@PeterRK

This comment has been minimized.

Copy link
Owner Author

@PeterRK PeterRK commented Sep 9, 2018

What I means "update its look” is to shorten 4 lines code to 1, reducing repetition.
@deanveloper I think I haven't gotten your point.

@odiferousmint

This comment has been minimized.

Copy link

@odiferousmint odiferousmint commented Feb 18, 2019

Usually, we don't need any custom exit function.

What do you mean? We sometimes would like to save the database to file, do some other cleanups related to the specific error itself (i.e. non-generalized), and so on. This would require us to have a custom exit function, wouldn't it? I personally believe that having the flexibility to have multiple groups of statements on error is a good idea. My issue with the check-tier proposal is that they all contain one single function that is being called, but that is inflexible, and I think it should have the ability to specify more than one handlers that you can call via check (one handler per check still).

@griesemer

This comment has been minimized.

Copy link

@griesemer griesemer commented Jun 4, 2019

PeterRK thanks for this write-up. I have referenced it from golang/go#32437.

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