The primary value of avoiding exceptions is that it makes error behavior explicit in the type of the function. If you’re in an environment where everything might fail, being explicit about it is probably a negative. But if most of your function calls are total, then knowing which ones might fail highlights places where you should consider what the correct behavior is in the case of that failure. Remember that the failure of an individual step in your program doesn’t generally mean the overall failure of your code.
It’s a little bit like null-handling in languages without options. If everything might be null, well, option types probably don’t help you. But if most of the values you encounter in your program are guaranteed to be there, then tracking which ones might be null be tagging them as options is enormously helpful, since it draws your attention to the cases where it might be there, and so you get an opportunity to think about what the difference really is.
A very prosaic example. In the stdlib,
Map.find throws an exception. In Base and Core,
Map.find returns an
Or_error.t. To me, the latter is clearly preferable, since it encourages the user to think about what they should do if
Also: recovering from exceptions is tricky. An exception that arrives in the middle of some imperative part of your program may trash your state by interrupting an update that wasn’t supposed to be interrupted. If your program is a compiler that is just going to shut down in that case, then that’s probably OK. But if your program is a long-lived system that wants to handle and recover from an exception, then it’s less reasonable to just keep soldiering on without understanding where precisely that exception occurred, particularly if the correctness of this program really matters.
I should say: I don’t mean to say that one should avoid exceptions entirely; there are definitely contexts where I think they’re the right way to go. But the value in having types capture error behavior seems very clear to me.