tags:

views:

267

answers:

4

How should exceptions be dispatched so that error handling and diagnostics can be handled in a centralized, user-friendly manner?

For example:

  • A DataHW class handles communication with some data acquisition hardware.
  • The DataHW class may throw exceptions based on a number of possible errors: intermittent signal, no signal, CRC failure, driver error. Each type of error gets its own exception class.
  • The DataHW class is called by a number of different pieces of code that do different types of acquisition and analysis.

The proper error handling strategy depends on the type of exception and the operation being attempted. (On intermittent signal, retry X times then tell the user; on a driver error, log an error and restart the driver; etc.) How should this error handling strategy be invoked?

  • Coding error recovery into each exception class: This would result in exception classes that are rather large and contain high-level UI and system management code. This seems bad.
  • Providing a separate catch block for each type of exception: Since the DataHW class is called from many different places, each catch block would have to be duplicated at each call site. This seems bad.
  • Using a single catch block that calls some ExceptionDispatch function with a giant RTTI-based switch statement: RTTI and switch usually indicates a failure to apply OO design, but this seems the least bad alternative.
+4  A: 

Avoid duplicating the catch blocks at each call site by catching (...) and calling a shared handler function which rethrows and dispatches:

f()
{
    try
    {
        // something
    }
    catch (...)
    {
        handle();
    }
}

void handle()
{
    try
    {
        throw;
    }
    catch (const Foo& e)
    {
        // handle Foo
    }
    catch (const Bar& e)
    {
        // handle Bar
    }
    // etc
}
fizzer
That would be my solution. Note that you don't need RTTI information, meaning that you won't need to add virtual methods to your exceptions just to get the compiler to generate RTTI.
David Rodríguez - dribeas
Exception handling depends on RTTI under the hood; this looks functionally identical to my third solution above, but it's much better style. Thanks.
Josh Kelley
+2  A: 

An idea I keep running into is that exceptions should be caught by levels which can handle them. For example, a CRC error might be caught by the function that transmits the data, and upon catching this exception, it might try to retransmit, whereas a "no signal" exception might be caught in a higher level and drop or delay the whole operation.

But my guess is that most of these exceptions will be caught around the same function. It is a good idea to catch and handle them seperately (as in soln #2), but you say this causes a lot of duplicate code (leading to soln #3.)

My question is, if there is a lot of code to duplicate, why not make it into a function?

I'm thinking along the lines of...

void SendData(DataHW* data, Destination *dest)
{
    try {
        data->send(dest);
    } catch (CRCError) {
        //log error

        //retransmit:
        data->send(dest);
    } catch (UnrecoverableError) {
        throw GivingUp;
    }
}

I guess it would be like your ExceptionDispatch() function, only instead of switching on the exception type, it would wrap the exception-generating call itself and catch the exceptions.

Of course, this function is overly simplified - you might need a whole wrapper class around DataHW; but my point is, it would be a good idea to have a centralized point around which all DataHW exceptions are handled - if the way different users of the class would handle them are similar.

aib
+1  A: 

There are three ways i see to solve this.

Writing wrapper functions

Write a wrapper function for each function that can throw exceptions which would handle exceptions. That wrapper is then called by all the callers, instead of the original throwing function.

Using function objects

Another solution is to take a more generic approach and write one function that takes a function object and handles all exceptions. Here is some example:

class DataHW {
public:
    template<typename Function>
    bool executeAndHandle(Function f) {
        for(int tries = 0; ; tries++) {
            try {
                f(this);
                return true;
            }
            catch(CrcError & e) {
                // handle crc error
            }
            catch(IntermittentSignalError & e) {
                // handle intermittent signal
                if(tries < 3) {
                    continue;
                } else {
                    logError("Signal interruption after 3 tries.");
                } 
            }
            catch(DriverError & e) {
                // restart
            }
            return false;
        }
    }

    void sendData(char const *data, std::size_t len);
    void readData(char *data, std::size_t len);
};

Now if you want to do something, you can just do it:

void doit() {
    char buf[] = "hello world";
    hw.executeAndHandle(boost::bind(&DataHW::sendData, _1, buf, sizeof buf));
}

Since you provide function objects, you can manage state too. Let's say sendData updates len so that it knows how much bytes were read. Then you can write function objects that read and write and maintain a count for how many characters are read so far.

The downside of this second approach is that you can't access result values of the throwing functions, since they are called from the function object wrappers. There is no easy way to get the result type of a function object binder. One workaround is to write a result function object that is called by executeAndHandle after the execution of the function object succeeded. But if we put too much work into this second approach just to make all the housekeeping work, it's not worth the results anymore.

Combining the two

There is a third option too. We can combine the two solutions (wrapper and function objects).

class DataHW {
public:
    template<typename R, typename Function>
    R executeAndHandle(Function f) {
        for(int tries = 0; ; tries++) {
            try {
                return f(this);
            }
            catch(CrcError & e) {
                // handle crc error
            }
            catch(IntermittentSignalError & e) {
                // handle intermittent signal
                if(tries < 3) {
                    continue;
                } else {
                    logError("Signal interruption after 3 tries.");
                } 
            }
            catch(DriverError & e) {
                // restart
            }
            // return a sensible default. for bool, that's false. for other integer
            // types, it's zero.
            return R();
        }
    }

    T sendData(char const *data, std::size_t len) {
        return executeAndHandle<T>(
            boost::bind(&DataHW::doSendData, _1, data, len));
    }

    // say it returns something for this example
    T doSendData(char const *data, std::size_t len);
    T doReadData(char *data, std::size_t len);
};

The trick is the return f(); pattern. We can return even when f returns void. This eventually would be my favorite, since it allows both to keep handle code central at one place, but also allows special handling in the wrapper functions. You can decide whether it's better to split this up and make an own class that has that error handler function and the wrappers. Probably that would be a cleaner solution (i think of Separation of Concerns here. One is the basic DataHW functionality and one is the error handling).

Johannes Schaub - litb
+1  A: 

Perhaps you could write a wrapper class for the DataHW class? The wrapper would offer the same functionality as the DataHW class, but also contained the needed error handling code. Benefit is that you have the error handling code in a single place (DRY principle), and all errors would be handled uniformly. For example you can translate all low level I/O exceptions to higher level exceptions in the wrapper. Basically preventing low level exceptions being showed to user.

As Butler Lampson said: All problems in computer science can be solved by another level of indirection