I was told catch statements should not be utilized to handle other business logic.
There is a lot of confusion on this point. People often refer to what other people said, but there are cases where others make blanket statements which are much broader than they need to be.
I don't like oversimplified rules (that omit any justification for why the rule is in place), I much prefer understanding the exact reason. So let me explain the reason why you should avoid using exceptions as much as you can.
For exception handling, this boils down to performance. Throwing an exception and catching it is rather expensive (performance wise). You are much better off using an intended return value as opposed to an exception.
As a simple example, I created a little algorithm that plays Tic Tac Toe. In some fringe cases, the AI refuses to make a move, and therefore resigns the game. I implemented this as a BotResignsException
. It worked very well when I initially tested this (playing manual games against the AI), but it crumbled once I started simulating bulk games (> 1 million games). The performance hit was massive.
I ended up rewriting the code to handle resignation without an exception, and the runtime of the application came down to just 14% of the original.
Intentional code paths are significantly more performant than clever exception catch logic.
Performance aside, there is also a good practice argument here. We've stopped using goto
statements because they lead to spaghetti code where the logical flow is incredibly hard to follow. Clever exception catching is effectively using the same goto
approach, albeit under a different name.
However, that doesn't mean that you should never catch
exceptions.
So why do we still use catch logic?
Because some exceptions are either unexpected or unavoidable. Some examples:
- Unexpected - The database server is down. We did not foresee this, so we couldn't prepare for it accordingly. Even if we were to write a manual check to see if the database server is online before we send our query, it's still possible that the server goes offline between our check and our actual query, so we can't actually prevent this problem from occurring. At this point, the minor performance hit of handling the exception is not as much of a problem, the priority is handling this exception gracefully and informing the user/admin of the situation.
- Unavoidable - Divide by zero exception. If you have a
double Divide(double a, double b)
method, it's impossible to return a value from it (i.e. a double
) when b
is zero, because whoever called the method cannot differentiate a correct number from an error number. The only option is to make it clear that you could not do the expected task (dividing by zero) is to raise an exception, because you specifically do not want to return any number.
- Avoidable -
IndexOf()
is a good example of a method which can avoid throwing an exception. Indexes are inherently postitive numbers. Therefore, when IndexOf()
returns a negative value, you know something is wrong. This is why IndexOf()
returns -1 when it can't find the specified element, as opposed to throwing a ElementNotFoundException
. It's functionally equivalent, but returning -1 is considerably more performant.
In this catch statement, I am then creating an error folder, and a file with data which caused issue. Where should I relocate these statements?
The first question you should ask yourself is how expected this error is.
If it's a commonly occurring error, you should not be using an exception. You should be checking for this erroneous state manually, and if the state is found to be erroneous, handle it accordingly.
You can get away with using an exception, if (and only if):
- Invalid data states are not common, they are rare fringe cases.
- There are many unforeseen possible errors (many reasons why a text line could not be parsed correctly), and you intend to catch all of them - not just your one specific issue. In other words, trying to write manual validation logic for every possible error state has become an exercise in futility because of the sheer amount of possible issues with the data.
- The performance hit of using an exception is not as important as being able to handle unexpected issues gracefully.
In most cases, you should be handling this manually. There are only few cases where all the criteria are met where it is acceptable to handle exceptions.
In either case, I suggest abstracting your catch logic. In your example:
catch
{
new ErrorLogger(filepath).WriteError(line);
}
This is an oversimplified example. The core idea is that you can hide the error handling logic, as it makes the rest of the method (the "happy" part of the method) longer and harder to parse. Abstracting the error handling logic improves both your readability and your separation of concerns.
Unrelated to the core of my answer;
- NEVER use
catch
(this is called Pokémon exception handling). I can't think of a single cases where you somehow want to catch every possible exception, yet are completely disinterested in what the exception actually is.
- Try to avoid using
catch(Exception)
(this is still Pokémon exception handling) unless you know you want to catch literally everything that could ever go wrong. Spoiler alert: in my 10 years of professional programming (i.e. using good practice), I've come across a single situation where I truly wanted to catch everything: To prevent exceptions from leaking to the consumer of my API.
- Try to always use
catch(ASpecificExceptionType)
so that you don't catch things you're not interested in handling.