Coding Codes

github.com/ucirello | @ucirello in gophers.slack.com

Critique on "return nil, err"

26 Feb 2017

If you have been writing Go code, it is very likely that you have written or read a piece of code that looked as following:

func letThereBeLight() (*Light, err){
	light, err := makeSomeLight()
	if err != nil {
		return nil, err
	}

	// do something with light...

	return light, nil
}

It is a kind of recursive pattern, mainly because every use of letThereBeLight will look like itself:

//...
	light, err := letThereBeLight()
	if err != nil {
		return err
	}

	// do something with the created light

	return nil
// ...

There is no proper name for this, but I like to call it “all-or-nothing error handling”. How would it be fundamentally superior than some C function return -1 or null on failure? All in all, both convey the idea that either the operation has been executed successfully or not at all.

int main() {

	child = fork();

	if (child == -1) {
		// fork was failed
		return 255
	}

	// ...
}

And the answer to this question is subtle. Namely if you have an extended exposure to a language where errors are either terminative or ignored. I leave to the reader the exercise to say which is which.

In these languages you are not induced or stimulated to treat the failures you get. Instead you must somehow be able to recover from them, and move on. Failures are all or nothing - either they are terminative errors, or they are not really errors. This is particularly problematic when the result of these failures is actually a corrupted state.

Go - and other languages with inline multi-valued returns - actually give and stimulate you to convey the corrupted state back to the caller. Thusly:

func letThereBeLight() (*Light, err){
	light, err := makeSomeLight()
	if err != nil {
		// light might be in some corrupted state, and you want to know of it.
		return light, err
	}

	// do something with light...

	return light, nil
}

The key concept is to remember to ask what a given error really means - and whether it must be fixed locally or bubbled out to the caller:

func letThereBeLight() *Light {
	light, err := makeSomeLight()
	if err != nil {
		light = new(Light)
	}

	// do something with light...

	return light
}

In this particular example, for some boring reason, light could be artifically created in case of failure, and thus it would make no sense to return the error found. And as important as fixing the return interface to convey the concept of partial failures, these must be properly exposed in documentation:

// letThereBeLight creates light by divine command. In case of errors, it creates
// an incomplete instance of light.
func letThereBeLight() *Light {
	light, err := makeSomeLight()
	if err != nil {
		light = new(Light)
	}

	// do something with light...

	return light
}

Or a more canonical example:

// from stdlib's html/template

// Must is a helper that wraps a call to a function returning (*Template, error)
// and panics if the error is non-nil. It is intended for use in variable initializations
// such as
//	var t = template.Must(template.New("name").Parse("html"))
func Must(t *Template, err error) *Template {
	if err != nil {
		panic(err)
	}
	return t
}

In this particular case, no given template should ever be invalid - and the right way to handle this problem is to panic. Or a more sophisticated example:

// from stdlib's io

// Reader is the interface that wraps the basic Read method.
//
// Read reads up to len(p) bytes into p. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered. Even if Read
// returns n < len(p), it may use all of p as scratch space during the call.
// If some data is available but not len(p) bytes, Read conventionally
// returns what is available instead of waiting for more.
//
// When Read encounters an error or end-of-file condition after
// successfully reading n > 0 bytes, it returns the number of
// bytes read. It may return the (non-nil) error from the same call
// or return the error (and n == 0) from a subsequent call.
// An instance of this general case is that a Reader returning
// a non-zero number of bytes at the end of the input stream may
// return either err == EOF or err == nil. The next Read should
// return 0, EOF.
//
// Callers should always process the n > 0 bytes returned before
// considering the error err. Doing so correctly handles I/O errors
// that happen after reading some bytes and also both of the
// allowed EOF behaviors.
//
// Implementations of Read are discouraged from returning a
// zero byte count with a nil error, except when len(p) == 0.
// Callers should treat a return of 0 and nil as indicating that
// nothing happened; in particular it does not indicate EOF.
//
// Implementations must not retain p.
type Reader interface {
        Read(p []byte) (n int, err error)
}

You may get read bytes (n int) even in the presence of error (err).

It takes a combined effort of creating interfaces that either do not leak errors to the caller with the action of interpreting full and partial errors, and treat them accordingly.

If not all errors are final, does it make any sense to treat them as such?