Exceptional Exceptions

This week I was introduced to the Elixir programming language. I'm super excited to try it out on a project. It's a scripting language built on erlang. The syntax looks a lot like ruby (yay!) but it's a functional language, feels a bit lispy, and it's got the concurrency benefits of erlang. Awesome.

I was reading the getting started documentation and came across the following in the section on exceptions:

Developers should not use exception values to drive their software. In fact, exceptions in Elixir should only be used under exceptional circumstances.

Notice that File.read does not raise an exception in case something goes wrong; it returns a tuple containing { :ok, contents } in case of success and { :error, reason } in case of failure.

This makes sense for a semi-pure functional language: if you're trying to minimize side-effects of functions, the possibility of a nonlocal jump with an exception is probably a bad thing. I felt like this was a bit extreme, though, in most cases. I remember when I was first learning about java exceptions long ago, they felt like a breath of fresh air. No longer was it necessary to set some sort of integer return code or something -- instead you could raise a named exception. Exceptions were readable and useful, and in java they were part of the interface (public void myFunction() throws MyAwesomeException), so you could do error handling well.

This weekend, I encountered a lot of problems with exceptions in production propagating way outside of the scope where they were raised. The issue was my code's fault, not the fault of how exceptions are used. Nonetheless, this got me thinking about whether exceptions are really not such a great thing. They have the property that they can cause nonlocal jumps in code, which is generally a bad thing because it can be hard to figure out how you got to where you are in the code. (This is one of the principal reasons that goto statements are a bad thing. Unlike exceptions, though, goto statements also make code exceptionally unreadable.) The fact that a function that I wrote can cause problems in code outside of that function that does not use the results of that function is not good. My overpropagating exception problems essentially boiled down to a "file not found". Why did my it have to be an exception? It's not unreasonable that a file might be missing. This shouldn't crash the whole program.

In all the programming languages I'm familiar with, there are generally two groups of exceptions: those that are expected to be caught by programs, and those that aren't. (e.g. in java RuntimeException and its subclasses vs. other exceptions). But if we're expecting some of the exceptions, why are we throwing them? Are they really exceptional? Is it worth the risk of nonlocal jumps to use them? Perhaps it works ok in java, where there are compile-time checks for non-runtime exceptions.

My issue was in python, however, which doesn't have such checks. (If you're willing to say that any language overuses exceptions, it's got to be python. Heck, iterators use exceptions to signal that they're done iterating! This is something normal that is expected from any finite iterator, not something exceptional!) While my code was at fault, it wouldn't have been a problem if a regular error that can be reliably dealt with didn't raise exceptions that can cause non-local program flow.

Maybe it's worth taking elixir's advice to heart and considering whether any time you're tempted to raise an exception, a different approach might work just as well, but not be as dangerous. Something to ponder at least!