Exceptions vs. Returns

Hugo Lopes Tavares
6 min readFeb 26, 2019

--

The other day Thiago Pontes shared a blog post with a few friends about exceptions considered an anti-pattern. I have a different opinion about exceptions and I thought it’d be interesting to write a blog post about it. I think exceptions are a great feature and the lack of it may cause harm!

The blog post my friend shared is Python exceptions considered an anti-pattern.

A life without exceptions

If you’ve ever worked with C code, you’ll remember those -1 and NULL return values indicating errors, or those times you had to remember to check the global errno to see if anything went wrong.

When a language doesn’t support exceptions and you call a function, it’s the caller’s responsibility to check if everything worked and handle any errors.

For example, the malloc() function returns NULL if it can’t allocate memory and you must check the return value:

int *p;
p = malloc(sizeof(int) * 100);
if (p == NULL) {
fprintf(stderr, "ERR: Cant allocate memory!");
exit(1);
}

Or this more evolved example from libcurl to check if a URL can be accessed with CURLE_OK indicating no errors:

#include <stdio.h>
#include <curl/curl.h>

int main(void)
{
CURL *curl;
CURLcode res;

curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
/* example.com is redirected, so we tell libcurl to follow redirection */
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);

/* Perform the request, res will get the return code */
res = curl_easy_perform(curl);
/* Check for errors */
if(res != CURLE_OK)
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));

/* always cleanup */
curl_easy_cleanup(curl);
}
return 0;
}

I used C examples because that’s the language I have experience checking for errors. However, this can be applied to any other language that doesn’t support exceptions, such as Go.

Golang and err

Go doesn’t have exceptions, but when writing a function, it’s common practice to return the result and a value indicating error, like how http.Get does it:

// func Get(url string) (resp *Response, err error)resp, err := http.Get("http://example.com/")

If there is any problem with the Get call, the variable err will contain the error information, or it’ll be nil. This is a great practice in Go and kudos for everybody doing that. If you lack exceptions, you must have conventions to indicate errors.

Let’s say you want to create a function that visits a URL and reads the response header Content-Type:

func GetContentType(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
return resp.Header["Content-Type"][0], nil
}

The previous function can be used like:

func main() {
contentType, err := GetContentType("http://example.com")
if err != nil {
fmt.Println("Found unexpected error", err)
} else {
fmt.Printf("Content-Type: %s\n", contentType);
}
}

If err is not nil, everything should be okay, right? No, it’s not right. If the Content-Type header can’t be found, resp.Header["Content-Type"][0] will panic with the error index out of range (the program will abort!). The previous handling wasn’t enough and because it wasn’t guarded with recover(), the program will crash. Having conventions for error handling isn’t enough; things can still go wrong and thinking that err checks are enough is an illusion.

Explicit error handling and chained calls

When errors are part of the return values, every function call must have error checks, handle the error if it knows what to do, or bubble it up to the previous caller (like GetContentType function returned err to its caller). If you’re using library A, you’re trusting the library authors handled every error possible. You’re also trusting the libraries that A depends on did everything right. At the end of the day, you’re trusting every library in the chain to do the right thing. It’s a lot of trust, isn’t it? If any library misses a check, you may have unknown results — this is a liability!

I’ve heard the argument that the liability isn’t that big of a deal if you use well-written libraries (but hey, how do you know the library is good? It’s still a liability, but well-written libraries minimize it).

I like the way Go programmers think about explicit error checks and they probably wouldn’t write the GetContentType like I did, with the explicit index access (I agree it’s a bug, but bugs happen). You can’t assume everything down the chain is properly implemented and checked — if you use third-party libraries that you can’t control, things may go south one day and your program will crash (this is the same problem that uncaught exceptions have).

Checked exceptions

Some languages, like Java, support checked exceptions. With checked exceptions, if a method can raise an exception, all of its callers must explicitly say that they can throw that exception. For example:

public void ioOperation(boolean isResourceAvailable) throws IOException {
if (!isResourceAvailable) {
throw new IOException();
}
}

The Java compiler will make sure that all methods that call ioOperation either have a try/catch for IOException or they have an explicit throws IOException clause as part of the method signature. This is a way to enforce proper checks. I wrote a bit of Java a decade ago and it was annoying to be explicit about all exceptions, but it can save your program. Compilers are a great help when you have static types and checked exceptions; they’re supposed to make your life easier.

Python doesn’t have checked exceptions and this can be seen as a problem because you can’t know before runtime if a function may raise an exception. The anti-pattern blog post talks about transforming all return values in Result objects, and with that, you could indicate exceptions types as part of the result and analyze the program statically with mypy for more safety. I think that’s an interesting idea for Python if you’re doing static type checking; the Java folks have been doing it for decades and it seems to work well. I’ve found a couple of tickets about that idea in mypy and typing, but it seems that the authors don’t care too much about that.

Safety

Can your program ever guarantee safety? I don’t think so. Even if you try/catch everything in your program, maybe a function in the chain of calls can try/catch and ignore errors. I don’t think it’s possible to guarantee safety, but I like the conventions some languages like Go are spreading. If your language supports exceptions, be mindful of them and decide if it’s your function’s job to handle them or if you should let them bubble up.

Unclear program flow (GOTO)

It’s undeniable that exceptions will cause your program flow not to be linear. Are exceptions a new way of writing GOTOs? Sort of. Exceptions are used mostly for error handling, but they are a way of changing program flow (independently of errors). With exceptions you can have a chain of calls such as A() -> B() -> C() and jump from C() to A(), bypassing B(). This looks like a GOTO, but it’s more powerful because you can have metadata associated with each jump (i.e., stack frames).

Dijkstra’s letter “Go To Statement Considered Harmful” is very famous, but many programmers took it too far saying that GOTO should never be used. The letter advocates structured programming, not the abolition of GOTO! The idea of GOTO being harmful is from a time where programmers were not using control flow structures such as while/for/if, all they knew was machine code, registers, and jumps (the letter is from the ’60s).

If you write large C programs and you try your best at safety, you’ll end up with either goto clauses or a lot of repeated code; pick your battles.

Conclusion

Exceptions are a language feature, not an anti-pattern. Be mindful of errors and try your best to ensure safety, but don’t forget that Safety is an illusion.

References

Originally published at blog.hltbra.net on February 26, 2019.

--

--