There are already so many articles about Go’s error handling so why write yet another one? Well I would like to show why I prefer returning errors over throwing exceptions in production grade code.
Let’s get started
Imagine that we have written some web service and it’s using storage which isn’t safe for concurrent access. With this problem in mind we decide to write this simple wrapper where we synchronize access to our otherwise non concurrent storage:
Now we have wrapped the underlying Add function call with sync.Mutex and made it safe to call from different goroutines. Some of you might think why not defer the Unlock() call. Well in this case we don’t want to pay the overhead of defer and clearly we can see that there is no actual need for it. Lock, Add, Unlock; clean, linear and easy to follow. Nice job!
The part of software that uses our code might look something like this:
The implementation of storage changes
Months go by and our sweet web service just keeps running nice and smooth. Satisfied with our work we decide to reward ourself with a nice trip to GopherCon. While heading down to San Diego our fellow gopher had to change the underlying storage implementation in a way which make’s it possible to return an error:
Now our colleague is trying to call NewSafeStorage with FailingStorage as an argument. But as we gophers know that wont do, and compiler is kind enough to tell us why:
*FailingStorage does not implement Storage (wrong type for Add method) have Add(string, string) error want Add(string, string)
With the help of compiler our fellow gopher is able to refactor our SafeStorage and see right away that now it makes perfect sense to defer the call of Unlock():
After refactoring the SafeStorage the final implementation might look something like this:
And there we have it! Explicit error handling made it easy and safe to refactor the codebase and our well deserved trip to GopherCon was able to continue without interruptions.
What if Go had exceptions instead?
Let’s imagine for a while that instead of errors we would have exceptions. Our code might look something like this:
Instead of explicitly saying that Add function returns an error we would have been throwing exception. Looks quite nice doesn’t it and we didn’t even have to change the signature of Add function! Since that signature didn’t change there is no reason to even look at SafeStorage implementation. Also the exception is handled so no reason for our compiler to cry about it, right?
Our fellow gopher deploys the change while we are getting ready for David Cheney’s speak at GopherCon. Just when the talk is about to start we get a message: SafeStorage has a Deadlock!
The problem with exceptions
In real world our fellow gopher would have most likely caught this error and fixed it right a way. But then again in the real world problems aren’t always this simple and errors might propagate through multiple layers with much more complex functions.
While the code base keeps growing and multiple people work on it, the chance that someone is catching the exception in wrong level get’s higher. And if I was able to demonstrate this problem with less than 100 lines of code, what are the changes of this happening when someone touches year old project with thousands of lines of code?
Exceptions allow functions to return in places you cannot see just by looking at the code. That behavior makes it harder to maintain the codebase.
Exceptions allow functions to return in places you cannot see just by looking at the code. That behavior makes it harder to maintain the codebase. For some Python script that might be completely fine but for production grade code I wouldn’t say so.
Two main reasons why I favor Go’s error handling model over exceptions are:
- It forces me to pay more attention to error handling
- It keeps the programs execution flow linear
The one argument in favor of exceptions is the amount of if err != nil checks you have to write when returning errors. This is something that is supposed to be taken care of in Go 2 with the checked expression.
In the end of the day, robust and maintainable codebase outweighs the ability to write bit less code, at least for me.
Thanks for reading!