Skip to content

Instantly share code, notes, and snippets.

@GGCristo
Last active July 13, 2022 03:05
Show Gist options
  • Save GGCristo/27c33308a07c1be216542f1005792c2b to your computer and use it in GitHub Desktop.
Save GGCristo/27c33308a07c1be216542f1005792c2b to your computer and use it in GitHub Desktop.
Simple Error Handling for Go 2. Part 2

This is my own iteration that build upon PeterRk's proposal. https://gist.github.com/PeterRK/4f59579c1162cdbc28086f6b5f7b4fa2
The idea is to reduce if err != nil {} boilerplate while not encouraging developers to write lazy error handling.

Go must still be Go

For me, this means:

  • Add as little syntax as possible
  • Be as explicit as convenient

Original idea

The user PeterRk proposed the next syntax:

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
}

with

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

being equal to

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

I think it's the right direction, and we can improve it.

What is wrong?:

  • Not explicit return statement
  • Sometimes the abstraction is unnecessary and make the code harder to read

New syntax

So with a simple change in the syntax, we get:

divisor, err := getDivisorFromDB(key) or return exit(err)

The or keyword detects if the last returned value (which must be an error) is different from nil, in which case executes the function in the right. We can omit return and the code will continue to execute. If we do so but exit return something it will be discarded just like in regular Go code, this way the function is more reusable.
Note that exit() is a regular call, so is very flexible with parameters/arguments.
And there is no need to consume err, actually thanks to that is orthogonal with the naked return, so we can do:

func GetDivisor(key string) (divisor uint, err error) {
	divisor, err = getDivisorFromDB(key) or return
	return
}

Special case: defer

I am not too sure if this is possible to implement or there are any weird corner case I am not contemplating, this section is just for debate sake, but we can take this opportunity to add error checking to defer. If we can make that or get triggered even if we don't save the returned error in a variable

defer f.Close() or return errHdl("", fmt.Errorf("couldn't close file"))

Or we can go even further and now think that it makes sense to save the variable (?)

defer err := f.Close() or return errHdl("couldn't close file", err)

Result

func Foo(path string) ([]byte, error) {
	errHdlr := func(reason string, err error) ([]byte, error) {
		return nil, fmt.Errorf("foo %s %w", reason, err)
	}
	
	f, err := os.Open(path) or return errHdlr("couldn't open file", err)
	defer f.Close() or return errHdl("", fmt.Errorf("couldn't close file"))
	result, err := io.ReadAll(f) or return errHdlr("couldn't read from file " + path, err)
	return result, nil
}

vs

func Foo(path string) ([]byte, error) {
	f, err := os.Open(path)
  	if err != nil {
    		return nil, fmt.Errorf("foo %s %w", "couldn't open file", err)
  	}
  	result, err := io.ReadAll(f)
  	if err != nil {
    		return nil, fmt.Errorf("foo %s %w", "couldn't read from file " + path, err)
  	}
	err = f.Close()
  	if err != nil {
    		return nil, fmt.Errorf("foo %s %w", "couldn't close the file " + path, err)
  	}
  	return result, nil
}

This is a very simple example, but we can already see the benefits. The programmer that is reading the code can even focus on the left side and ignore error handling. What do you think about gofmt aligning ors?

f, err := os.Open(path)      or return errHdlr("couldn't open file", err)
defer f.Close()              or return errHdl("", fmt.Errorf("couldn't close file"))
result, err := io.ReadAll(f) or return errHdlr("couldn't read from file " + path, err)

Drawbacks

The problem comes when a user abuse of this system when good old if != nil {} is enough. We can guess that lazy programmers (as myself) or those who are in a hurry or just still learning (it's me again :D) prefer to write an if more than a regular or anonymous function.
Or we can make the opposite approach and allow a single return line next to or, but I don't like that that much, it would bring too many inconsistency because there would be situation where if != nil {} would still be the more appropriate, in other words just too many ways of doing the same things with subtle differences, we obviously want to avoid that.

@divinerapier
Copy link

Thanks to this code I was cured of my low blood pressure.

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