Skip to content

Instantly share code, notes, and snippets.

@posener
Last active February 12, 2024 05:32
Show Gist options
  • Save posener/a303becac35835ad7bf5e15fe061893e to your computer and use it in GitHub Desktop.
Save posener/a303becac35835ad7bf5e15fe061893e to your computer and use it in GitHub Desktop.
Function Failure reporting: Error or OK

Function Failure Reporting: Error or OK

Go's "multiple return values" feature, can be used for several purposes. Among them for failure reporting to the function caller. Go has two conventions for failure reporting, but currently, no clear convention for which to use when. I've encountered different programmers that prefer different choices in different cases. In this article, we will discuss the two, and try to make the process of choosing our next function signature more conscious.

The Basics

Let's introduce the two failure reporting conventions.

Error

It is common to see a function signature that returns an error, like os.Chdir(dir string) error, or a pair of value and error, like os.Open(name string) (*File, error). A function that returns such a value, actually says: "I can't handle what just happened, you handle it". Usually, when a called function has such a signature, a gopher will check the error value and handle it.

f, err := os.Open("my-file.txt")
if err != nil {
  // handle error
}
// use f

This is one of the nicest things in Go! The error handling is explicit, clear, and done in an "indent error flow". A nice article about how a gopher should handle errors can be found in Dave Chaney's blog. My favorite part in the article is "Only handle errors once".

OK bool

Another way to report the caller about a failure, is to return a bool, or a pair of a value and a bool, Like http.ParseHTTPVersion(vers string) (major, minor int, ok bool). By convention, such a function will use named return values. The last returned value in the function signature will be called ok of type bool and will indicate if the function failed. When a called function has such signature, a gopher will check the ok value after the call:

minor, major, ok := http.ParseHTTPVersion("HTTP/1.1")
if !ok {
  // handle failure
}
// use minor and major

Here again, the handling of the failure is very clear, and is done in an indented block as well.

I'm Confused

I am about to write a new function, what will be its signature?

If the function is guaranteed to complete successfully, and this is either by having no way to fail, or by handling failures in it, there is no reason to return either an error or an OK bool. But what if the function can fail, and its scope can't deal with that failure? as previously discussed, we have two options here. Let's consider them carefully.

When my function is very simple, I can use the OK bool. It can fail, but when it fails, the caller knows why. Good examples from the go language are not function calls - but languge syntax, which use the OK bool convention:

  1. The interface assertion: val, ok := foo.(Bar), if the ok value is false, it is clear that foo is not of type Bar, we don't have any other option here.
  2. The map's key testing: val, ok := m[key], if ok is false, it is clear to the caller that the key is not in the map.

An example that demonstrates the downside of such signature is the implementation of the ParseHTTPVersion function. The function returns a false value in 4 places, so at least 4 different inputs can result in a false value due to different reasons. The caller has no information about the failure, just the fact that it happened.

On the other hand, when opening a file, there could be many reasons for failure, the file might not exist, the user might not have permissions for the file, and so on, and this is why os.Open returns a file object and an error pair. In case of a failure, the returned object in the error interface will contain the reason, and the caller can understand how he could handle the failure.

We can come to the straightforward conclusion that returning an error is more informative. A value that implements the error interface, can be called with its Error() function and a string that represents the error will appear to the perplexed gopher.

Additionally, we can say that the group of functions that OK bool can be used as the failure indicator is a subset of the group of functions that error can be used as their failure indicator. In any case where an OK bool is chosen, it can be replaced with an error interface. But not the other way around, if we replace an error with OK bool, we loose information, and the caller could not handle all the cases he could before.

Popularity: A simple grep on the standard library (not too accurate) shows that there are about 7991 functions that use the error interface as a return argument, and about 236 functions that use a last returned argument named ok of type bool.

APIs

Your functions are your APIs. And as we all know, we shall not break them. Even the breaking of internal functions that are used widely in your package can result in painful code refactoring. The function's API consists of its name, its arguments, and the return values. Assuming you are writing a very simple function, that might fail, and because it is so simple, you ought to give it an OK bool return value. A very simple indication of success that makes elegant if-statements after the function call. In most cases that would be a wise decision, in terms of APIs.

But in some "border" cases of functions that are not as simple as they first seem, more logic is needed to be injected into the function, and more failure cases are introduced. Still, at every failure case it will return false, assuming we are not willing to break API. The calling function, that might log the failure, will have no choice, but to notify that error: 'foo' failed. which will result in a program that is much harder to debug. If the calling function is trying to handle the failure, it might handle some cases in a wrong way, just because it didn't get all the information.

Using the error interface, is much more flexible. Since it is an interface, you can return anything that implements it, and any caller will know what to do, and will have more information about what exactly failed in your function.

The Interface Cost

error is an interface, which has a performance penalty over a bool variable. This performance penalty can be considered minor, depending on the function purpose. This might be a good reason to prefer an OK bool return type. An interesting debate about interfaces performance can be found here.

Messy code

When some functions return error as a failure indicator, and some return an OK bool, a function that is a bit complex and uses several function calls can look a bit ugly:

a, err := foo()
if err != nil {
  log.Println("foo failed:", err)
}
b, ok := bar() 
if !ok {
  log.Println("bar failed")
}
err := baz()
if err != nil {
  log.Println("baz failed:", err)
}

Conclusions

In this article, I propose making conscious decisions in selecting functions failure reporting type. It would be awesome to have good guidelines or conventions for the failure indicator right return type.

As I can see it, and in my opinion, the default choice for failure indication should be the error interface. It is explicit, flexible, and can be used everywhere - even where you might want to use the OK bool type.

Guidelines to prefer an OK bool over error:

  1. It is very very very clear what ok == false means.
  2. It is performance critical to prefer bool over error.

I would love to hear your opinions about this subject. If you agree, disagree, or have in mind more aspects of this problem, please comment below.

Thanks for reading,

If you find this interesting, please, ✎ comment below or ★ star above ☺

For more stuff: gist.github.com/posener.

Cheers, Eyal

@tmornini
Copy link

tmornini commented Aug 1, 2017

It's worth noting that the ok convention may not indicate failure, but instead Indicate action taken -- or not.

@posener
Copy link
Author

posener commented Aug 1, 2017

@tmornini, can you please give an example for such function?

@AshleyFinney
Copy link

Perhaps you could extend this article to include if and when to use rogue return values? Such as returning -1 from a search function.

type intSlice []int

func (g intSlice) indexOf(i int) int {
  for n, v := range g {
    if v == i {
      return n
    }
  }
  return -1
}

@posener
Copy link
Author

posener commented Aug 4, 2017

@AshleyFinney, thanks for reading and for the comment. That's a good point, to my opinion those functions are unicorn :-) special cases that the shortcut of not returning an error can pay off, or special functions that programmers are so used for the convention, that it is clear to leave them as is.

Notice that part of go's error return value purposes is to get rid of those conventions, Quoting from effective go:

One of Go's unusual features is that functions and methods can return multiple values. This form can be used to improve on a couple of clumsy idioms in C programs: in-band error returns such as -1 for EOF and modifying an argument passed by address.

In C, a write error is signaled by a negative count with the error code secreted away in a volatile location. In Go, Write can return a count and an error: “Yes, you wrote some bytes but not all of them because you filled the device”. The signature of the Write method on files from package os is:

func (file *File) Write(b []byte) (n int, err error)

and as the documentation says, it returns the number of bytes written and a non-nil error when n != len(b). This is a common style; see the section on error handling for more examples.

@microwaves
Copy link

microwaves commented Sep 1, 2017

I agree with @tmornini. One example that I faced recently was that I wanted that a function returned if the rollback was executed or not, plus the string output.

func deployOrRollback(foo, bar string) (string, bool) {
	o, err := executeDeploy(foo, bar)
	if err != nil {
		o = rollback()
		return o, true
	}
	return o, false
}

@posener
Copy link
Author

posener commented Sep 8, 2017

@microwaves, in your case, the returned value is not an indication of failure, as far as I can understand.
Usually, in those cases, when a function has several return values, and it can be unclear, I use named return values.
For example, your function will be much clearer with this signature:

func deploy(foo, bar string) (deployName string, rollbacked bool)

@sorenvonsarvort
Copy link

sorenvonsarvort commented Jun 16, 2018

Imho, it is okay to return 3-values only for lookup-like functions, when nil result posible, because there can be no actual errors happened while lookup and no value can be found.

func lookupSomething() (LookupResult, bool, error)

All other cases do not need return 3 values, if we pack result-values to a structure.

@gladuo
Copy link

gladuo commented Jan 7, 2020

good one

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