void MyClass::EstablishAllConnections()
{
try {
f(); // f() may throw ArithmeticException
g(); // g() may throw NetworkConnectionException
h(); // h() may throw DatabaseException
}
catch (ArithmeticException &ae)
{
HandleArithmeticException(ae);
}
catch (NetworkConnectionException &nce)
{
HandleNetworkingException(nce);
}
catch (DatabaseException &dbe)
{
HandleDatabaseException(dbe);
}
}
Now there are several problems with this code. To see it, consider how
you would write another function, that also used the g() to establish
another network connection. Of course, you would write something like this:
void MyClass::PromptHostAndConnect()
{
try
{
a(); // a() may throw UserInputException.
g(); // g() may throw NetworkConnectionException
}
catch (UserInputException &uie)
{
HandleUserInputException(uie);
}
catch (NetworkConnectionException &nce)
{
HandleNetworkConnectionException(nce);
}
}
See? Now there are two copies of exactly similar duplicated code - but only
as part of an exception handler. Even though we've carefully factored
the common code into a separate function HandleNetworkConnectionException,
there are still significant amount of duplicated code in these functions.
We can do better. First, we move most of the exception handling code
into a single function:
void MyClass::HandleExceptions()
{
try
{
throw;
}
catch (UserInputException &uie)
{
// ...
}
catch (NetworkConnectionException &nce)
{
// ...
}
catch (DatabaseException &dbe)
{
// ...
}
catch (ArithmeticException &ae)
{
// ...
}
}
Now we've built a function, which will first rethrow the active exception,
then catch it, and handle it properly. Now we can use it to invoke the
reusable exception handlers as follows:
void MyClass::EstablishAllConnections()
{
try {
f(); // f() may throw ArithmeticException
g(); // g() may throw NetworkConnectionException
h(); // h() may throw DatabaseException
}
catch (...)
{
HandleExceptions();
}
}
Note that here, we do not need to know the exact type of the exception -
the 'HandleExceptions' function can take care of all that for us.
This technique, although it is very simple, is often overlooked. Maybe this is because it's too easy to write a separate exception handler for all functions. Or maybe it's because nobody thought of reusing those exception handlers. Or it might be because not all compilers have traditionally supported this. Nonetheless, abstracting away common exception handlers is very efficient means of clearing up exceptions-filled code.
The easy way of getting at it is:
void MyClass::EstablishAllConnections()
{
try {
f1(); // f1() may throw ArithmeticException
}
catch (ArithmeticException &ae)
{
cleanup1();
cleanupAll();
}
try {
f2(); // f2() may also throw ArithmeticException
}
catch (ArithmeticException &ae)
{
cleanup2();
cleanup1();
cleanupAll();
}
}
[Note: the name "cleanup" is not intended to convey that this would
only reprsent cleanup actions, you can also think of those as
placeholders for the exception handling code. Note however that there
are portions in both catch blocks that are exactly the same. This is
unavoidable in this kind of code.]
See how messy this approach gets? Even though we again went to great lengths
trying to ensure we have factored the common code into a separate
functions, we ended up in an unreadable mess. When you have a sequence of
five or more of these, the size of the function is already too unwieldy
to maintain. So something better is needed. Here's a better
alternative:
void MyClass::EstablishAllConnections()
{
int operationsDone = 0;
try {
f1(); operationsDone = 10;
f2(); operationsDone = 20;
}
catch (...)
{
if (operationsDone >= 20) { cleanup2(); }
if (operationsDone >= 10) { cleanup1(); }
cleanupAll();
}
}
Here we use a separate integer variable for keeping track of which operations
have been successfully completed. This will separate error handling code
away from the actual algorithm, and therefore eases understanding and
readability of this code.
I received a comment from Yves Dufournaud that it is possible to
improve the above a bit:
void MyClass::EstablishAllConnections()
{
int operationsDone = 0;
try {
f1(); ++operationsDone;
f2(); ++operationsDone;
...
}
catch (...)
{
switch( operationsDone)
{
... // in reverse order
case 2: cleanup2();
case 1: cleanup1();
default: cleanupAll();
}
}
This is a form of simplification to the previous approach, intended to
make the code even clearer than the previous. It should be noted
however that there are several subtle aspects related to the choice
between these versions. First,
[2]Glen McCluskey & Associates LLC has a report of a ANSI/ISO standards meeting discussing this idea.
$Id: reusable-exception-handler.html,v 1.4 1999/06/20 13:19:40 esap Exp $