On Exceptions

Programmers are generally aware of two primary methods of exception handling. You have the old C-style error code method:

int hMissileControl;
int iReturnCode = launchMissiles(&hMissileControl);
if (iReturnCode == ACCIDENTALLY_HIT_INNOCENTS)
    printf("My bad.");
else
    controlMissile(hMissileControl);

You also have the more common throw/raise paradigm:

MissileController missileController;
try {
    missileController = launchMissiles();
    controlMissile(missileController);
} catch (AccidentallyHitInnocentsException)
    printf("My bad.");

This is the predominant paradigm in popular languages like C++, Java, Python, and Ruby.

I’ve heard complaints from both sides. The C-stylers posit, “throwing exceptions introduces all sorts of hidden, complex return paths in your program!” They’re right. With exceptions, a simple program like the following cannot be guaranteed to be simple. There is much more than meets the eye.

void main() {
    doSomethingSimple();  // this might throw
}

Who knows what doSomethingSimple might throw? When does it throw? Can you guarantee it won’t throw with certain inputs?

Furthermore, there’s the classic edition of the hated “blind man’s catch”:

try
    somethingComplicated();
catch (...)
    printf("Something horrible happened...no idea what.");

Polymorphic exception handling opens the door to horrendous laziness like the sample above. Programmers can simply wrap code in a huge sarcophagus-sized band-aid and hope everything turns out alright. Of course, programmers ought to only catch the exceptions they know they can handle, but instead of doing all that research, they figure catching all exceptions under heaven will suffice.

But the C-style method comes with its own catch (pun intended). Exception throwers rightly argue that error codes have a potentially worse side-effect: code is innocent until proven guilty. Now, in our democracy, “innocent until proven guilty” is a jolly good thing to have. But in programming languages, it’s nigh deadly. What I mean is this: in a C-style program, you ignore errors by doing nothing at all. If you want to know an error happened, you have to explicitly check your return codes. But in a throw-style program, the only way to ignore an error is to explicitly catch it. Blindly ignoring errors is a deadly thing to do in programming, and that’s what C-style programming gives you by default.

Take these two programs for example:

// C-style
initializeStuff();  // this returns an error code if it fails
doTheRest();

// throw-style
initializeStuff();  // this throws if it fails
doTheRest();

Do you see the difference? The code is identical. But one of these programs will blindly doTheRest even if the initialization failed. Then when doTheRest explodes miserably, the programmer has to weed through debug logs (if they’re lucky) or use a debugger (if they’re luckier) to find out what happened. But the other program dies instantly with a clean stack trace describing what happened.

So which is better, C-style or throw-style? I don’t know. There have probably been studies to answer this question, but I haven’t read them.

Fortunately, there’s a third way that has the best of both worlds. It doesn’t introduce all sorts of hidden return paths through your code, but it also doesn’t let you blindly keep chugging along when your engine just blew. Because Haskell does such a elegant job of achieving this third way, I’ll use it as an example.

Let’s use sqrt (square root function) as an example. The sqrt function takes integers as input and returns a floating point number as its result. In Haskell that would look like this:

sqrt :: Int -> Double  -- what do we do on negative ints?

The only problem is that sqrt doesn’t work on negative numbers (unless you handle complex numbers, but let’s pretend you don’t feel like doing that right now). So when sqrt is handed a negative number, what do you do? The C-stylers would say that you need to return an error code (and unkindly force the output to a pointer-ridden input argument). The throwers would say to throw. But this third way does neither. Look at the new type signature:

sqrt :: Int -> Maybe Double  -- I might work, I might not, depending on what you give me.

This new type signature means that sqrt might return a Double-floating point number, or it might return Nothing.

Instead of throwing, Haskell’s type system lets you wrap your return value in a thing called a Maybe. This Maybe thing basically tells the compiler that the value will either be a Double or Nothing.

Now, if you are doing normal things with your sqrt function, everything will work jolly well. But if you accidentally give it -453, it will return Nothing and everything will halt. Of course, like the throwing paradigm, you can, if you want, handle this Nothing case and keep on moving. But, by default, your program will not ignore the problem.

This gives you the best of both worlds. Your functions only have one return path, but your program can’t blindly keep trucking if something goes horribly wrong.

Haskell has other types like Maybe that allow functions to return more than just Nothing, perhaps an error description or an error code.

Haskell’s common method of error handling isn’t confined to just Haskell, though. With a little OOP magic, it’s easy to make functions in C++ that have similar behavior. But Haskell’s system is especially good at it, which is why it’s so common.

Maybe the next time you want to handle errors, you’ll consider Maybe.

2 Comments

  • Great breakdown! Very succinct critique of return codes and exceptions, and a good introduction to Maybe.

  • Thank you. The discussion began at work and just got me thinking. There’s a lot of hand-waving when it comes to the introduction of Maybe, but it works for a quick synopsis. The downside to type-enforced error conditions is that they tend to muck up your type signatures and propagate into functions you were hoping to keep simpler (assuming you want your error to cascade).

Leave a Reply

Your email is never shared.Required fields are marked *