Apparently the fact that I use exceptions in my suggested control flow mechanism for collections makes me a bad person. This idiocy touches on a sore point for me. I have been invited to write a post on “Why exceptions for control flow are a good idea”. So. Here it is.
The thing is, I really can’t be bothered. I can give and have given useful use cases, but I’ll just be told that they’re evil because I use exceptions. So instead I will argue the converse: Why they are not a bad idea.
Let’s consider some common objections:
Exceptions are slow
No they’re not. Exceptions in C++ (and I believe .NET) may be slow, but many implementations of exceptions are in fact extremely fast. The various ML implementations have taken advantage of this for quite some time. The JVM too (with some mild tricks) has very fast exceptions.
Exceptions are for exceptional situations
This argument is pervasive. It’s also meaningless. Depending on the intent of the one claiming it, it is one or more of three different forms of fallacy:
- Assertion of the conclusion
- Argument by name. They’re called exceptions, therefore they must only be for exceptional conditions. If I called them kittens would it suddenly be ok to use them for control flow? (Next post: Kittens for control flow considered harmful. I look forward to seeing Josh Suereth’s rebuttal).
- Argument by meaningless blither. What distinguishes “exceptional conditions” from “control flow”? I have reached the end of a million element list. This happens one time in a million! That sounds pretty exceptional to me!
Writing exception safe code is hard
No it’s not. Be sure to clean up anything that could need cleaning up in a finally block. If you write decent code, and you’re using a language with automatic memory management so that this doesn’t apply to Joe Random Object, these tend to be small and relatively well isolated cases. Where possible you abstract this out entirely by using patterns like automatic resource blocks (remember: In decent languages it’s just a library).
Further, even if it were true, this would be an argument against using exceptions at all rather than using them for control flow: If you aren’t sure where exceptions may be thrown from (which is particularly true if you’re writing generic library code), you have to write exception safe code.
It’s confusing
Everything’s confusing when you first encounter it. Then you get used to it, you find where it does and doesn’t work, and then it becomes non confusing. As long as you continue to maintain that a technique is evil and should not be used it will remain confusing.
Debuggers will break on exceptions
Decent debuggers allow you to specify which exceptions you break on and which you don’t. It’s easy to provide a marker class that says “This exception is for control flow. Don’t worry about it”.
You might accidentally catch the exceptions you’re using for control flow with too broad exception handling
Consider the break/continue example. What happens if I do the following (and break is implemented with exceptions):
for(thing <- myThings){ try { doStuff; if(dieNow) break; } catch { case (e : Exception) => e.printStackTrace } }
We accidentally catch the break! This is bad.
There are a couple aspects to my response to this:
First off, given sensible library design it doesn’t happen at all. Control flow exceptions get moved outside the hierarchy of normal exceptions which it’s sensible to catch (in my quick hacked together version they were Errors. Really they should be subclasses of ControlFlow which extends Throwable).
Secondly, this is a problem with too broad exception handling: You’ve handled a generic exception where you probably just wanted to handle a specific one. This is always known to be bad.
Thirdly, this is a really really easy problem to spot. As soon as you even lightly test the code you will see a stack trace for Break being printed and will realise what you’ve done wrong. This might cost you a good 10 seconds of confusion tops. (If you simply swallow the exception silently this isn’t true. But if you do that I have no sympathy for you at all and your code deserves to break).
The problems with using exceptions for control flow are well documented
Actually, they’re not. If you don’t believe me, check google scholar.
Edit: It has been pointed out to me that if you add quotes to the search term then you do get a few hits. They all seem to be random Java best practices documents deriving from Rules for developing robust programs with java exceptions, which does not appear to present any arguments not already covered here. For completeness, here is the relevant section from it:
IM1: Do not use exceptions for control flow.
It is possible to use the exception handling mechanism for control flow also during normal
program flow. This makes the code harder to read, harder to analyze, significantly slower and
opens up for some horrible situations if an actual exception were to occur. Exception should,
as the name states, only be used in exceptional situations.
So we’ve got one count of “confusing”, one count of “performance”, one of what I can only imagine is a vague allusion to the problem of accidentally catching the exception and a chaser of “Exceptions should only be used in exceptional circumstances”. This, as far as I can tell, is the gold standard argument for the opposition in the Java realm.
Feel free to post additional arguments for or against in the comment. I’ll expand the article with any good ones posted.
I think the ‘ve mostly heard the second reason (“Exceptions are for exceptional situations”) and usually the 3rd point there seems to be the problem: people think that ‘exceptional situations’ are only situations that they couldn’t anticipate. Of course, that way of thinking completely defeats the purpose of checked exceptions.
I think a clear counterexample is given by the way FileNotFoundException is used in the java.io libraries. They could have required that the truths of file.exists(), file.isFile() and file.canRead() are asserted before invoking new FileInputStream(file) and having it fail spectacularly if the file is ‘invalid’. However, it is often a reasonable assumption that a file will be ‘valid’, because it’s a constant in your config, because the user selected it in a FileChooser, etc. In those cases an ‘invalid’ filename is *exceptional*, although it is a condition that can be expected to occur every once in while, for instance because savvy users modify a chosen filename in the FileChooser. Your code is clearer, because it isn’t cluttered by all those low-level calls and if it fails, it is reasonably clear why it failed. In some ways, an Exception is pretty much a typed errorcode.
This is what Sun has to say about the subject: http://java.sun.com/docs/books/tutorial/essential/exceptions/advantages.html
First, I don’t find the performance and debugging arguments against using exceptions for control particularly interesting either. I don’t think I’d use exceptions this way, but I do use actors, for instance, and so it applies to me regardless of break/continue. Consequently, I find the broad exception handling argument interesting.
I agree that you should not catch Throwable in general and not really even Exception. However, the major Scala projects that I can think of are all frameworks in some way (specs, ScalaCheck, Lift, …) and have to run/call user code with unknown properties. Then, it might be legitimate to trap a LinkageError in one test in order to continue running other tests, for example. In fact, I think only three Errors should never be trapped (InternalError, UnknownError, and probably ThreadDeath), even in frameworks.
Now, in order to properly handle user code, I need to catch Exceptions and Errors, except for the three above. Other Errors should only be handled in some circumstances (LinkageErrors as long as it isn’t a problem with the framework itself) and a StackOverflowError or OutOfMemoryError may or may not be a reason to exit. So, I agree that proper exception handling is important, but I don’t agree that it is not hard (define hard as difficult to get right and probably include time consuming as well).
Relating this more directly to exceptions as control flow, I agree that a ControlFlow subclass of Throwable is necessary. If I’m doing the above handling properly, I don’t see accidentally catching ControlFlow happening. If I accept that, I would want ControlFlow in the standard library for visibility and official sanctioning (otherwise, the effort arguing about it will negate the benefits).
In summary, I seem to like the idea of being able to define control mechanisms in a library using some sort of stack unwinding. I wouldn’t accept thinking about if/else this much, though, so perhaps what I want is compiler magic to ensure the ControlFlow exception can’t be caught in user code? Maybe I want exception visibility qualifiers? Oh well.
-Mark
Mark:
One to think about is that when you’re talking about a framework which calls user code and then returns from the framework, you probably *do* want to catch ControlFlowExceptions.
Most of the use cases for these involve unwinding to some previous point in the user stack. So a case where an exception intended for control flow unwinds out of the user code and back into framework code probably is a user error, as it’s pretty strongly likely that the framework isn’t intending to catch it!
To put it another way, the issue is as follows:
handlingCode {
tooGeneralCatch{
throwingCode;
}
}
The following does not cause a problem:
tooGeneralCatch {
handlingCode{
throwingCode;
}
}
Which is the setup you’d probably find in a framework like SBT, ScalaCheck, etc.
You can easily go too far in either direction with this.
Exceptions -are- for exceptional situations because that is what they were designed to be. Language implementors and library authors, thus rightfully consider themselves free to build poorly performing implementations of things like error object constructors (e.g. eagerly building error message strings). API users, then, must not assume that raising an exception is a performance-optimal thing to do.
That said, more than 90% of the code in most applications is not performance-critical, so doing the simplest possible thing will get your job done the most quickly – so you actually have time left to optimize the parts that turn out to actually need it.
In the example about checking for file existence, trapping an exception is clearly a good choice. Otherwise, there is a finite chance that the file status could change between the existence check and the attempt to use it, and then an exception would still occur, and you should still have a check for it. Do you actually want to code an explicit check and an exception handler for the same case? Also, we’re talking to a filesystem, so presumably that’s the bottleneck, and not the overhead that might be present in the exception handling process.
This would happen if user code uses break without an enclosing breakable (or whatever), right? If I trap control exceptions, then I have to know about the ones I don’t want to catch.
For example, I have actors for running tasks in parallel. In the actor body, it calls a task, with the call wrapped in exception handling. Now I have to know to catch ControlFlowException but not ActorControlFlowException or whatever actors use (guess I need to go read the code).
Mark and David:
I think this discussion about user vs framework code brings up a potential problem with using exceptions for control flow. David is right that frameworks probably should want to catch ControlFlow exceptions, except he doesn’t point out that they probably don’t want to catch them in their own control flow constructs! This would make user error subtly break framework code, and worse, do so silently. Consider the following:
def frameworkCode = for (x T): T = try { x } catch { case e: ControlFlow => throw LeakedControlFlowException(e) }
def frameworkCode = for(x <- xs) safe(userCode(x))
If such a change is introduced into the standard collections library, then all existing framework code will be made incorrect.
Hmm… Half of my comment got eaten up last time. Let me try again.
Mark and David:
I think this discussion about user vs framework code brings up a potential problem with using exceptions for control flow. David is right that frameworks probably should want to catch ControlFlow exceptions, except he doesn’t point out that they probably don’t want to catch them in their own control flow constructs! This would make user error subtly break framework code, and worse, do so silently. Consider the following:
def frameworkCode = for (x <- xs) userCode(x)
If a ControlFlow exception “leaks” from userCode, then frameworkCode will be silently incorrect. Framework authors would need to be careful to catch ControlFlow exceptions in user code and make sure to loudly propagate it up in a way that indicates to the user that their code is leaking a ControlFlow exception.
def safe[T](x: =>T): T = try { x } catch { case e: ControlFlow => throw LeakedControlFlowException(e) }
def frameworkCode = for(x <- xs) safe(userCode(x))
If such a change is introduced into the standard collections library, then all existing framework code will be made incorrect.
Your blog ate my comment because of my failure to properly html-escape my angle brackets. Ironic given the subject I addressed.
First off, not arguing against your main thesis – that implementing exceptions for control flow is a viable option. I’m not prepared to argue fervently for it either. My concern is that you’ve buried a critical caveat in a linked article. The key points of the article should be featured prominently in the main body of your post.
Under the “Exceptions are Slow” heading you say
The JVM too (with some mild tricks) has very fast exceptions.
The parenthetical comment, “with some mild tricks”, links to a very critical caveat – Java exceptions are much faster:
IF you write custom exception classes
IF you explicitly disable stack trace generation in your custom classes
IF you exclusively use custom exception classes for control flow
In contrast, Exceptions that were designed for error handling do generate stack traces. I have found more then a few examples of code implementing control flow using exceptions designed for error handling.
A better title for your post would be:
“Custom exceptions for control flow considered perfectly acceptable, thanks very much”
The new initial word is key.
A final observation – you argue that the phrase “Exceptions are for exceptional situations” is meaningless. The phrase may sacrifice strict accuracy for catchiness, and there are certainly ambiguous cases. But in day-to-day programming, I don’t spend time anguishing over whether a condition is an exception condition or a control flow condition. The distinction between the two is clear; I handle control flow the old-fashioned way and throw exceptions for errors.
Yes, I agree with your addendum about custom exceptions: Most exceptions whose intended use case is actual error reporting should not be used for control flow. I was sortof implicitly assuming that, but I should have stated it outright. Even if one ignores the performance issues (which are still *relatively* mild), treating errors as control flow conditions which you don’t feel the need to report on is a very dubious practice which can mask some genuine errors (and I’ve seen this happen in real life. Sigh)
David, I just found/read this article (and the previous one) and wanted to point out that there are existing languages which very successfully use exceptions for control flow.
One of these is Python, which mainly does it at two levels:
* Python’s Iterator protocol is made of two methods __iter__ and next(), __iter__ returns the iterator and next() returns the next item in the iterator. You’ll notice there is no has_next() method, and that’s because when calling next() on an expanded iterator, the iterator simply throws StopIteration.
* Error handling/condition checking (or the lack thereof). The Python community broadly subscribes to the EAFP principle: it’s easier to ask for forgiveness than permission, so if you want something just do it and if things break clean up.
One of the classic example of this is fetching a value (which may or may not be there) from a map. Java code will pretty much always take the form
if(map.containsKey(someKey)) {
T value = map.get(someKey);
} else {
// stuff to do if someKey wasn’t in the map
}
while Python code, though it will be found in this form as well (tends to vary based on the number of misses expected, mostly) will very often read:
try:
value = my_dict[some_key]
except KeyError:
# stuff to do if some_key isn’t in my_dict
And as an aside, that doesn’t mean “Exceptions are for exceptional situations” is meaningless, the issue here is what people consider “exceptional circumstances”. I tend to like Python’s approach of “yeah when you’re iterating having a next item is normal. Not having one is exceptional”.
Hi Masklinn,
You’re quite right: This is hardly a new point (I’m slightly surprised I didn’t mention this). There are a bunch of languages which already do it. OCaml and, to a lesser extent, Haskell also do.
As far as the exceptional circumstances thing goes: I’d say that it’s meaningless as an argument regardless of whether it’s meaningful as a statement, because as your example shows there’s a lot of wiggle room in what you consider to be exceptional.
> As far as the exceptional circumstances thing goes: I’d say that it’s meaningless as an argument
And I’d completely agree with that.
Pingback: A manifesto for error reporting | David R. MacIver
Pingback: Best of drmaciver.com | David R. MacIver