Back in the late 90’s I worked quite a bit with a code base that used exceptions as flow control. It implemented a finite state machine to drive telephony applications. Lately I am reminded of those days because I’ve been doing MVC web apps.
They both have Controller
s that decide where to go next and supply the data to the destination logic. User actions from the domain of an old-school telephone, like DTMF tones, became parameters to action methods, but instead of returning something like a ViewResult
, they threw a StateTransitionException
.
I think the main difference was that action methods were void
functions. I don’t remember all the things I did with this fact but I’ve been hesitant to even go down the road of remembering much because since that job, like 15 years ago, I never saw this in production code at any other job. I assumed this was a sign that it was a so-called anti-pattern.
Is this the case, and if so, why?
14
There’s a detailed discussion of this on Ward’s Wiki. Generally, the use of exceptions for control flow is an anti-pattern, with many notable situation – and language-specific (see for example Python) cough exceptions cough.
As a quick summary for why, generally, it’s an anti-pattern:
- Exceptions are, in essence, sophisticated GOTO statements
- Programming with exceptions, therefore, leads to more difficult to read, and understand code
- Most languages have existing control structures designed to solve your problems without the use of exceptions
- Arguments for efficiency tend to be moot for modern compilers, which tend to optimize with the assumption that exceptions are not used for control flow.
Read the discussion at Ward’s wiki for much more in-depth information.
See also a duplicate of this question, here
28
The use case that exceptions were designed for is “I just encountered a situation that I cannot deal with properly at this point, because I don’t have enough context to handle it, but the routine that called me (or something further up the call stack) ought to know how to handle it.”
The secondary use case is “I just encountered a serious error, and right now getting out of this control flow to prevent data corruption or other damage is more important than trying to continue onward.”
If you’re not using exceptions for one of these two reasons, there’s probably a better way to do it.
6
Exceptions are as powerful as Continuations and GOTO
. They are a universal control flow construct.
In some languages, they are the only universal control flow construct. JavaScript, for example, has neither Continuations nor GOTO
, it doesn’t even have Proper Tail Calls. So, if you want to implement sophisticated control flow in JavaScript, you have to use Exceptions.
The Microsoft Volta project was a (now discontinued) research project to compile arbitrary .NET code to JavaScript. .NET has Exceptions whose semantics don’t exactly map to JavaScript’s, but more importantly, it has Threads, and you have to map those somehow to JavaScript. Volta did this by implementing Volta Continuations using JavaScript Exceptions and then implement all .NET control flow constructs in terms of Volta Continuations. They had to use Exceptions as control flow, because there is no other control flow construct powerful enough.
You mentioned State Machines. SMs are trivial to implement with Proper Tail Calls: every state is a subroutine, every state transition is a subroutine call. SMs can also easily be implemented with GOTO
or Coroutines or Continuations. However, Java doesn’t have any of those four, but it does have Exceptions. So, it is perfectly acceptable to use those as control flow. (Well, actually, the correct choice would probably be to use a language with the proper control flow construct, but sometimes you may be stuck with Java.)
11
As others have mentioned numerously, (e.g. in this Stack Overflow question), the principle of least astonishment will forbid that you use exceptions excessively for control flow only purposes. On the other hand, no rule is 100% correct, and there are always those cases where an exception is “just the right tool” – much like goto
itself, by the way, which ships in the form of break
and continue
in languages like Java, which are often the perfect way to jump out of heavily nested loops, which aren’t always avoidable.
The following blog post explains a rather complex but also rather interesting use-case for a non-local ControlFlowException
:
- http://blog.jooq.org/2013/04/28/rare-uses-of-a-controlflowexception
It explains how inside of jOOQ (a SQL abstraction library for Java) (disclaimer: I work for the vendor), such exceptions are occasionally used to abort the SQL rendering process early when some “rare” condition is met.
Examples of such conditions are:
-
Too many bind values are encountered. Some databases do not support arbitrary numbers of bind values in their SQL statements (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). In those cases, jOOQ aborts the SQL rendering phase and re-renders the SQL statement with inlined bind values. Example:
// Pseudo-code attaching a "handler" that will // abort query rendering once the maximum number // of bind values was exceeded: context.attachBindValueCounter(); String sql; try { // In most cases, this will succeed: sql = query.render(); } catch (ReRenderWithInlinedVariables e) { sql = query.renderWithInlinedBindValues(); }
If we explicitly extracted the bind values from the query AST to count them every time, we’d waste valuable CPU cycles for those 99.9% of the queries that don’t suffer from this problem.
-
Some logic is available only indirectly via an API that we want to execute only “partially”. The
UpdatableRecord.store()
method generates anINSERT
orUPDATE
statement, depending on theRecord
‘s internal flags. From the “outside”, we don’t know what kind of logic is contained instore()
(e.g. optimistic locking, event listener handling, etc.) so we don’t want to repeat that logic when we store several records in a batch statement, where we’d like to havestore()
only generate the SQL statement, not actually execute it. Example:// Pseudo-code attaching a "handler" that will // prevent query execution and throw exceptions // instead: context.attachQueryCollector(); // Collect the SQL for every store operation for (int i = 0; i < records.length; i++) { try { records[i].store(); } // The attached handler will result in this // exception being thrown rather than actually // storing records to the database catch (QueryCollectorException e) { // The exception is thrown after the rendered // SQL statement is available queries.add(e.query()); } }
If we had externalised the
store()
logic into “re-usable” API that can be customised to optionally not execute the SQL, we’d be looking into creating a rather hard to maintain, hardly re-usable API.
Conclusion
In essence, our usage of these non-local goto
s is just along the lines of what Mason Wheeler said in his answer:
“I just encountered a situation that I cannot deal with properly at this point, because I don’t have enough context to handle it, but the routine that called me (or something further up the call stack) ought to know how to handle it.”
Both usages of ControlFlowExceptions
were rather easy to implement compared to their alternatives, allowing us to reuse a wide range of logic without refactoring it out of the relevant internals.
But the feeling of this being a bit of a surprise to future maintainers remains. The code feels rather delicate and while it was the right choice in this case, we’d always prefer not to use exceptions for local control flow, where it is easy to avoid using ordinary branching through if - else
.
Using exceptions for control flow is generally considered an anti-pattern, but there are exceptions (no pun intended).
It has been said a thousand times, that exceptions are meant for exceptional conditions. A broken database connection is an exceptional condition. A user entering letters in an input field that should only allow numbers is not.
A bug in your software that causes a function to be called with illegal arguments, e.g. null
where not allows, is an exceptional condition.
By using exceptions for something that is not exceptional, you are using inappropriate abstractions for the problem you are trying to solve.
But there can also be a performance penalty. Some languages have more or less efficient exception handling implementation, so if your language of choice does not have efficient exception handling, it can be very costly, performance-wise*.
But other languages, for example Ruby, have an exception-like syntax for control flow. Exceptional situations are handled by the raise
/rescue
operators. But you can use throw
/catch
for exception-like control flow constructs**.
So, although exceptions are generally not used for control flow, your language of choice may have other idioms.
* En example of a performance costly use of exceptions: I was once set to optimize a poorly performing ASP.NET Web Form application. It turned out, that the rendering of a large table was calling int.Parse()
on approx. a thousand empty strings on an average page, resulting in approx. a thousand exceptions being handled. By replacing the code with int.TryParse()
I shaved off one second! For every single page request!
** This can be very confusing for a programmer coming to Ruby from other languages, as both throw
and catch
are keywords associated with exceptions in many other languages.
0
Programming is about work
I think the easiest way to answer this is to understand the progress OOP has made over the years. Everything done in OOP (and most programming paradigms, for that matter) is modeled around needing work done.
Every time a method is called, the caller is saying “I don’t know how to do this work, but you do know how, so you do it for me.”
This presented a difficulty: what happens when the called method generally knows how to do the work, but not always? We needed a way to communicate “I wanted to help you, I really did, but I just can’t do that.”
An early methodology to communicate this was to simply return a “garbage” value. Maybe you expect an positive integer, so the called method returns a negative number. Another way to accomplish this was to set an error value somewhere. Unfortunately, both ways resulted in boilerplate let-me-check-over-here-to-make-sure-everything’s-kosher code. As things grow more complicated, this system falls apart (or must be very carefully managed).
An Exceptional Analogy
Let’s say you have a carpenter, a plumber, and an electrician. You want to plumber to fix your sink, so he takes a look at it. It’s not very useful if he tells only you, “Sorry, I can’t fix it. It’s broken.” Hell, it’s even worse if he were to take a look, leave, and mail you a letter saying he couldn’t fix it. Now you have to check your mail before you even know he didn’t do what you wanted.
What you would prefer is to have him tell you, “Look, I couldn’t fix it because it seems like your pump isn’t working.”
With this information, you can conclude you want the electrician to take a look at the problem. Perhaps the electrician will find something related to carpenter, and you’ll need to have the carpenter fix it.
Heck, you might not even know you need an electrician, you might not know who you need. You’re just middle-management in a home repair business, and your focus is plumbing. So you tell you’re boss about the problem, and then he tells the electrician to fix it.
This is what exceptions are modeling: complex failure modes in a decoupled fashion. The plumber doesn’t need to know about electrician- he doesn’t even need to know that someone up the chain can fix the problem. He just reports on the problem he encountered.
So… an anti-pattern?
Ok, so understanding the point of exceptions is the first step. The next is to understand what an anti-pattern is.
To qualify as an anti-pattern, it needs to
- solve the problem
- have definitively negative consequences
The first point is easily met- the system worked, right?
The second point is stickier. The primary reason for using exceptions as normal control flow is bad is because that’s not their purpose. Any given piece of functionality in a program should have a relatively clear purpose, and co-opting that purpose leads to unnecessary confusion.
But that’s not definitive harm. It’s a poor way to do things, and weird, but an anti-pattern? No. Just… odd.
5
It’s completely possible to handle error conditions without the use of exceptions. Some languages, most notably C, don’t even have exceptions, and people still manage to create quite complex applications with it. The reason exceptions are useful is they allow you to succinctly specify two essentially independent control flows in the same code: one if an error occurs and one if it doesn’t. Without them, you end up with code all over the place that looks like this:
status = getValue(&inout);
if (status < 0)
{
logError("message");
return status;
}
doSomething(*inout);
Or equivalent in your language, like returning a tuple with one value as an error status, etc. Often people who point out how “expensive” exception handling is, neglect all the extra if
statements like above that you are required to add if you don’t use exceptions.
While this pattern happens to occur most often when handling errors or other “exceptional conditions,” in my opinion if you start seeing boilerplate code like this in other circumstances, you have a pretty good argument for using exceptions. Depending on the situation and implementation, I can see exceptions being used validly in a state machine, because you have two orthogonal control flows: one that’s changing the state and one for the events that occur within the states.
However, those situations are rare, and if you’re going to make an exception (pun intended) to the rule, you had better be prepared to show its superiority to other solutions. A deviation without such justification is rightly called an anti-pattern.
2
In Python, exceptions are used for generator and iteration termination. Python has very efficient try/except blocks, but actually raising an exception has some overhead.
Due to lack of multi-level breaks or an goto statement in Python, I have at times used exceptions:
class GOTO(Exception):
pass
try:
# Do lots of stuff
# in here with multiple exit points
# each exit point does a "raise GOTO()"
except GOTO:
pass
except Exception as e:
#display error
4
Let’s sketch such an Exception usage:
The algorithm searches recursively till something is found. So coming back from recursion one has to check the result for being found, and return then, otherwise continue. And that repeatedly coming back from some recursion depth.
Besides needing an extra boolean found
(to be packed in a class, where otherwise maybe only an int would have been returned), and for the recursion depth the same postlude happens.
Such an unwinding of a call stack is just what an exception is for. So it seems to me a non-goto-like, more immediate and appropriate means of coding. Not needed, rare usage, maybe bad style, but to-the-point. Comparable with the Prolog cut operation.
3
The “pro-antipattern” argument is not very strong and is having it’s own internal debate:
The same reference used to support the antipattern case can also be found to support the benefits of exceptions:
https://wiki.c2.com/?UseExceptionsInsteadOfErrorValues
Further, I believe the case made that it is an antipattern is pretty weak:
From the oft-referenced wiki:
The best example is a for loop with no constraint? Who would do this?
On that very same page he admits to using exceptions for flow control:
“Flow control” by itself is overly generalized
The term “flow control” has limited use as it describes the meat and potatoes of programming. There is no black and white here, only shades of gray.
Sure, it makes little sense to catch an exception raised by Parse when a TryParse exists. But that does not mean that the tester-doer pattern needs to apply to every method. Do we really need a TrySave()? Or can we say that there is some level of granularity that represents a tipping point for when we should be able to simply code for the successful case (should Save() simply continue if it failed??)?
In the absence of exceptions, what choice do you have?
I guess if this is an antipattern but the same reference is indicating that return codes should be avoided (I consider true/false return codes), what are we supposed to do?
- Exceptions introduce a completely unnecessary, heavy performance hit that you don’t ever need to take if you just use a return code (like most APIs do)
- Programmers tend to consider exceptions as “something that just doesn’t/or very rarely happens”, so they often just dump all “bad” program state to an exception block with no recovery code other than a printStackTrace/send this in to our developers.
0