Exceptions Part 1: What is the Purpose of Exceptions?
A few months ago I wrote some long comments on an Eric Lippert blog post and ever since I’ve been meaning to distill them into something more coherent.
Furthermore, any mature language (indeed, any mature product) is like a blurred photograph of something in motion, or like an evolved animal. It contains strange vestigial limbs and other leftovers from its evolutionary past. And there is no reason to suppose that the evolution has stopped. This is a domain that somehow refuses to sit still, stop fidgeting and behave itself while we all attempt to understand it properly.
So even once you’ve grasped the importance of understanding exceptions properly in your choice of language (which many don’t), the next challenge is distilling that understanding from the fragments of good and bad advice, or mere clues, scattered around you.
So I’ll tackle this as a series of articles, and gradually turn this post into the contents page, plus part 1.
- Exceptions Part 1: What is the Purpose of Exceptions?
- Exceptions Part 2: Why do we need to catch them?
- Exceptions Part 3: Why do we need ‘finally’ blocks?
- Exceptions Part 4: What happens if ‘finally’ blocks always execute?
- Exceptions Part 5: The myth of the exception hierarchy
I’m going to start with something that seems incredibly simple and basic, but which many working programmers don’t agree on. Some people say exceptions represent errors (what kind of errors?) Others say they represent exceptional – that is, rare – conditions (how rare?) And yet others say they shouldn’t be used; they’re more trouble than they’re worth. All these people are wrong!
Exceptions provide a way for a routine to communicate a result; in that way they appear to compete with return values, and those who have a beef with exceptions often point to special return values as the obvious alternative. In reality they are very different, but related, things.
A function, in mathematical terms, is a way of getting a new value from an existing one. In programming languages, the “return value” of a function is how we communicate the value of a function back to the caller. This allows functions to be “composed”:
y = f(g(h(x)));
x is passed
h, and the result of that is passed to
g, the value of which is passed to
f, the ultimate result being stored in
Suppose you write some code to figure out a person’s age. As a pure function, it would need two parameters, the current date and the person’s date of birth. We only need to pass in the person’s date of birth, because we can look at the clock (in other words, we’re not using a pure functional language). And given the purpose of the function, it is unquestionable that the return value should be the age of the person. That’s the whole point of return values. Let’s say it’s an integer measuring years, in the traditional way.
But suppose the date object happens to represent a time in the future. This is meaningless – we don’t talk about the age of someone who hasn’t been born yet. In that case our function should have no value at all – it should be undefined. This is a really important part of the concept of functions: that they have a domain of allowed inputs, outside of which they simply don’t have a value.
We could choose a so-called “magic” value to represent an undefined result. Let’s see, we’ve already said that negative ages don’t make sense, so we aren’t using the negative range of integers. So we can use -1 as our magic value that means “undefined”. Problem solved!
Well, sort of. But now the next piece of code in the chain will receive an age of -1, and will try to treat it as meaningful, because the coder didn’t take the trouble to check for the magic value. The default, the easiest thing to do, is to ignore the problem. This is a thoroughly rotten situation; it means that the side-effects of a bug are able to spread throughout the state of a program, causing mysterious symptoms that may be far removed from the original cause.
So exceptions reverse this situation. They allow us to write functions that have a well-defined domain, such that the default behaviour where the boundary of that domain is breached is for the program to stop running and display some kind of complaint, the correct interpretation of which is: “fix me, you idiot.”
In order to cope with the situation automatically, the programmer must explicitly “opt in” by handling the exception. This is a really important aspect of good design: choosing a sensible default. Making it so that when people aren’t paying attention, they happen to wander in a reasonable direction.
Even in C, which has no exceptions in the language, we nevertheless hope that dereferencing a NULL pointer will cause a “core dump” or “access violation” message and halt execution. So from that perspective, we can think of exceptions as allowing us to extend that kind of robust checking by default to other types besides pointers.
And it goes without saying that it is advantageous for a function with no value to be able to state this in a standard, unambigous way, instead of trying to encode the concept of “undefined” in some magic value, which would need to be different depending on the value’s representation.
Void Functions and State Changes
So we have a clear justification for exceptions where we have a return value. As we’re not talking about a pure functional language, what about functions that return
void? These are functions that modify some state. But we can understand what an exception means here by thinking in pure functional terms and then translating back to imperative concepts.
Imagine that a
void function accepts the current state of the entire program as a hidden parameter, and returns the new state as a hidden return value. This sounds comically inefficient, as we only need to keep the most recent state. But conceptually it’s the same thing, and it lets us use the same justification for exceptions: a function throws when it cannot produce any return value. Translating back to impure terminology, the
void function was unable to completely finish modifying the program state. And so if we’re going to be really helpful in writing void functions, they better behave like the functional equivalents: if they can’t make the complete change, they ought to make no change at all.
Next time: If we’re really being rigorous, should we even be allowed to catch exceptions in our programs?