Exceptions Part 2: Why do we need to catch them?
See the Contents Page for this series of articles
Last time we established that an exception is the way a function unambiguously produces no value – or no side-effect, which amounts to the same (no)thing. To take the best known example from mathematics, the function
f(x) = 1/x has no value if
x is zero. If you’re asking what anything divided by zero is, well… you shouldn’t be asking. Just don’t ask.
But by catching an exception, you are saying “I expect this”. So to be entirely rigorous about it, isn’t it wrong that we ever try to catch these things? Maybe, like in mathematics, we should never attempt to evaluate them in the first place.
Suppose we want to open a file. We want to avoid exceptions, so we first check if the file exists, and if it doesn’t we never try to open it. But that doesn’t cover the case where the user doesn’t have the right permissions. So we add another check for the permissions. And so on, until we’ve basically reproduced all the validation that is going to be done again anyway when we eventually attempt to open the file. Sounds like a waste of effort, and it is, but that’s not the worst of it.
The more fundamental problem goes by the terrifying name shared mutable state under concurrency. And this is just a fact of life that cannot be worked around, even if you don’t care about performance. You could do all those checks before you try to open the file, but then in that particular nanosecond another process could sneak in and delete the file, so all your checks were pointless. The answer you came up with, “we’re good to go”, was a lie, and in general must always be a lie. The file system is a mutable resource, shared across many separately processes, with no common approach to synchronization. This is a pretty good description of the state of the universe too. It means we cannot predict with certainty whether something will work, unless we completely and accurately model all other processes in the universe, which is clearly not practical. We can only attempt some operation and then we’ll know afterwards if it worked or not.
Now, I know what you’re thinking. Functional programming languages surely don’t have this problem. But in practice they do, because on closer examination they’re not so pure after all (see the bottom of this page). “A programing language ain’t no kinda programming language if he don’t know what time it is, bro”, as I believe Sir Charles Antony Richard Hoare once said. Useful programs have to act on things that they do not already have complete information about. Complete information is too expensive to wait for. It’s more cost effective to make a reasonable guess.
Even so, just because we can’t avoid sometimes expecting exceptions, that doesn’t mean we can pretend that we expect all exceptions. There should be many that we do not expect, and therefore should not catch.
Crashing Is Good
I’ve noticed that people by nature aren’t interested in the subject of unhandled exceptions. They tend to think like this:
An unhandled exception means my program is going to crash. So it’s already the worst possible situation, and so what do I care about the fine details of how it crashes? I have more important things to worry about than that!
But once you’ve spent a decade or so trying to support real software products “in the wild”, you get a different perspective. Exactly how a product crashes is hugely important. It needs to halt in an unambiguous way, not try to stagger on embarrassingly. And it needs to leave behind plenty of clear evidence. It should halt as soon as it detects an unexpected condition. Crashing, relatively speaking, is awesomely helpful. What is much worse is failing to crash when you should. A program that keeps running when it is an unknown state is a dangerous rogue agent.
The mistake is in thinking that losing what is in the memory of one process (which is what happens in a clean crash) is the worst that could possibly happen. How about losing what is in the file system, or the database? Bugs that trash persistent data are potentially far more destructive. And what about destroying the evidence needed to identify and fix the bug? That means the bug has to occur to real users hundreds of times before you are lucky and it happens to crashes in a helpful way. And so catching an exception and attempting to recover can be hundreds of times more disastrous than not catching the exception.
So now we’ve established the landscape of exceptions. In summary, so far:
- We need exceptions because sometimes functions don’t have values (or cannot complete a state change)
- We need to be able to catch some exceptions because real-world functions are often timing sensitive. We don’t control all their inputs, so we can’t predict when they will succeed.
- We need to be able to not catch some exceptions because they tell us our program is in an unknown state and it’s time to quit before we can do more serious damage.
All very clear and rational. Now it’s time for the messy stuff.
Next time: Why do we need