Exception Handling Alternatives

Exception Handling Alternatives

By Detlef Vollmann

Overload, 7(30):, February 1999


After having read a lot of C++ exception discussions in the previous issues of Overload, I will introduce some alternate methods of handling exceptional events.

Exceptional events occur. But, since they are exceptional, they occur very rarely. This is exactly the problem with them. In programming courses you learn to always handle any possible event. But, in practice, most programmers just ignore them. If you look at this problem in detail, you see that these events are not ignored where they actually occur, but at some higher level. For example, a "Disk Full" error is detected in the output routine (perhaps printf or operator<<) and is flagged, but the calling routine ignores the error. This is exactly why the exception mechanism was introduced into C++. You just cannot ignore an exceptional event after it has been detected and thrown. But, as has been discussed in previous articles, exceptions have other problems. As I wrote earlier, the exception mechanism of C++ is necessary as a substitution for the C longjmp mechanism. But, I still think this standard mechanism is not good enough for working with exceptional events. So, I will suggest some alternative mechanisms that can handle at least some of the events with greater ease.

Example

As a running example, I will use a typical framework system that implements the Builder pattern from the GoF ("Gang of Four", nickname for the authors) book [ GoF ].

The application is a CASE graphical editor, and the framework provides classes to read in a design diagram from a file in a specific format (e.g. CDIF) and to build an internal representation of the diagram. The framework provides an abstract base class Input, which has a pure virtual function readChar() that must be implemented by the application developer in a derived class. This function might encounter a problem on reading a character, and the framework designer must decide how this problem shall be reported to the framework and the application. Of course, end-of-input is not an exceptional event and therefore must be handled by normal processing, probably by a special read character returned.

(BTW, this design is a non-template implementation of the Iterator pattern. But there are not many differences if you decide to use a templated approach).

class Input
{
public:
// The derived class might open a file in 
// the constructor, or a network 
// connection, ...
  Input();  
// No (empty) exception specification 
// here, because you never know what the 
// destructor of the derived class might 
// do here.  But I will not discuss this 
// issue further in this article.
  virtual ~Input();  

// About the exact declaration see the 
// text.
  virtual SomeReturn readChar() = 0; 
};

Another part of the framework provides the classes to build the internal representation. An abstract base class DiagramBuilder is provided that declares the virtual functions for building the parts of the diagram. The application must subclass DiagramBuilder and implement these functions.

class DiagramBuilder { public: // ... virtual void buildEntity(EntityInfo&) = 0; virtual void buildRelation(RelationInfo&) = 0; // ... };

Return Codes

The first alternative to standard C++ exceptions I will present are return codes. This was extensively discussed by Ken Hagan and The Harpist in the last issue.

If you must solve the example problem with return codes, you might find a special character to return in case of a problem, but a better solution would be to declare readChar like this:

ReturnStatus readChar(char &c);

Here, readChar returns the read character in c and reports a problem as a return value of type ReturnStatus, which might be a simple enum or a complete object containing all interesting information for the problem. In the latter case, the return value should be returned by reference, as the returned object might be of some class derived from ReturnStatus.

For DiagramBuilder, the functions must be declared to return a status (probably again a derivation of ReturnStatus by reference).

The problem with this approach is that the framework does not know what to do with the error, and must now unwind the stack manually until it can return the error status to the application. This is the classic problem of error handling in frameworks, where error detection and error handling are absolutely not local.

But, where you could perhaps use return codes quite well is inside the framework. In this aspect, I perhaps agree with Ken Hagan. Where error handling is local, and you are in control of both the source of the exceptional event and the handling of it (and, of course, your source is not inside of a constructor or an overloaded operator), then return codes are probably the best technique for handling exceptional events. But, you must be sure that you are really in complete control of both the source and handling points, i.e. neither of them is inside a virtual function that may be overwritten in a derived class. So, on second thoughts, for frameworks where most classes may be specialized by the user of the framework, return codes are not a good idea for handling exceptional events.

The main problems with the return codes approach are:

  1. They are unusable for constructors, destructors and overloaded operators.

  2. They are difficult to propagate to the place where the event can be handled consistently for the whole application. In our example the problem occurs inside an application function, and it must be handled by the application, but the framework, which doesn't know what kind of problems might occur, must itself propagate the error.

  3. Programmers ignore them.

Deferred Error Handling

Another possibility for handling an error is "deferred error handling" (as I have seen first proposed by James Kanze in [ Kanze1 ]). This technique is used by the "classic" iostream-library. Here, an object internal error flag is set, if a problem occurs. This flag can be checked by clients using a special member function (e.g. overloaded !-operator). All member functions of the class check the flag and act respectively (probably by doing nothing).

In our example, readChar() could set an internal flag, and return end-of-input. This would cause the framework either to report an error ("premature end of file") or just to finish with an incomplete representation. In both cases the application has to check for the flagged error and must handle it appropriately. To be able to identify the error correctly, the flag must not only be a binary value but probably a full exception object.

If a function of DiagramBuilder detects a problem, again a flag is set, but the function will return normally. So, the framework will not notice that an exceptional event occurred and will just continue to create the diagram. But, all subsequent calls to one of the member functions of DiagramBuilder will see the set error flag and just do nothing. After the framework has completed the diagram, it will return to the application, which will check the error flag and handle it.

In all cases, the framework has nothing to do with the handling of any of the exceptional events; it won't even notice when one has occurred.

One problem arises with the location of the flag: the objects for Input or DiagramBuilder may no longer exist when the diagram is completed. So, the flag must be in an object that still exists after the completion of the building process. For DiagramBuilder, this would obviously be the created diagram representation, but for Input it is difficult to find a good location, as the input object itself is created by the framework and only visible from there.

The advantage of deferred error handling is the invisibility to the non-interested parts of the application. In our framework example the errors are completely transparent.

A disadvantage of deferred error handling is the long time, which might pass between the detection and the handling of an error. The application must be carefully written to ensure that endless loops are not created. For example, if the framework needs some information from the diagram representation to complete its task, this information must be given.

Deferred error handling is best used for classes whose responsibilities are non-critical, e.g. output for purely informational logging. In this case the application is run until successful completion, and than the error flag is checked and some notification message might be issued.

Error Stack

David Vandevoorde proposed in [ Vandevoorde ] the usage of a global error stack. This is effectively a variant of the standard C++ exception mechanism, but without hidden control flows and everything (including stack unwinding) must be done explicitly. When a problem is detected, instead of throwing an exception, or to flag it internally as in the deferred error handling mechanism, it is pushed down onto a global stack (in a multi-threaded environment, you have to decide whether you want one stack per thread or one per process). Then, each critical section (or the member functions of critical objects) must check the error stack to ensure the consistency of the manipulated object. If a problem is pending (there is some exception on the stack), nothing is done. So far this is similar to the deferred error handling mechanism. But, any function can decide to handle the exception and pop it from the stack. This is similar to the standard exception handling mechanism. Where you would have a catch clause, you now just check for an entry on the stack.

Both our example framework and application can make use of this mechanism. If only the application were to use an error stack then the readChar and application DiagramBuilder functions would put an exception on the stack when a problem occurs, and they would check for an exception on entry and act as in the deferred error handling mechanism. When the framework eventually returns control to the application, the application would check for any pending exceptions on the stack.

If the framework also implemented the error stack mechanism, then the application would have to use the same stack, and the readChar and the buildX functions would have to push their exceptions on to this same stack. The framework then typically checks the stack for exceptions directly after each call to one of the functions provided by the application. If an exception is pending it has to unwind the stack manually (i.e. return from all its functions) and return control to the application, which then has to handle the exception. In this case, the framework reports its own problems to the application also via the exception stack.

The error stack mechanism is essentially the same mechanism as standard C++ exception handling. You typically cannot do anything useful as long as there are unhandled exceptions. Your only actions when you have hanging exceptions are in most cases clean-up actions and manual stack unwinding (returning from the function). But, you have no hidden stack unwinding without your control, and you can have more than one exception at the same time on the stack, so you don't have artificial (i.e. coming from the C++ environment and not from the problem domain) problems with exceptional events in destructors.

Since the error stack is not a standard mechanism of C++, you cannot assume that any 3rd party code will use this technique. And, of course, you lose the capabilities of the built-in mechanisms. So, if the constructor of one element of an array fails, but this failure is only reported by an exception on the stack, all other elements are constructed as well, and you have to destruct them again manually. But, this is more a problem of efficiency and not of the principle. Another problem is to find the best match for the error handling action. This is a typical multiple dispatch problem, as you have two criteria (the place of the handling and the type of the exception) for which you must find the best matching action. With C++ exceptions, the C++ environment helps you to define this match. With an error stack, you have to do it yourself, and you must use some technique as described in the GoF Visitor pattern (or one of its many variants). Yet another problem is the possibility of ignoring the exception and performing unsafe actions on inconsistent objects. But, this can (at least partially) be avoided by the mixing with the deferred error handling mechanism.

An error stack is effectively a mix of all the other techniques. It has elements of C++ exceptions, return codes and deferred error handling. It also has its own mix of pros and cons: No hidden control flow, but it can be ignored, no problems with exceptions from destructors, but no compiler supported best match of handling action. Also, as it is a rather unknown technique I would not use it as the standard exception handling mechanism for the framework. But, it might be a good choice for the application part of our example (i.e. the transfer of errors from readChar and buildX through the framework back to the application.

Safe Termination

Often, a safe termination of the program is the best approach. James Kanze provided in [ Kanze2 ] the following categorization of applications:

  1. Critical: if this fails, a backup system will step in

  2. Small standalone programs like compilers: if they crash they will just be restarted by the user

  3. Other; e.g. this might be a server in a client/server application: it should not crash, but there is no special fail-over environment.

While on first consideration a lot of applications seem to belong to the third category, on closer inspection most fall into categories (1) or (2). For example, a server might spawn a new process for each client. If that process crashed then either the client must be restarted (then the server belongs to category (2) because it only serves this client), or the server is must be restarted automatically (by a process monitor) and the client connection must be automatically recreated; in this case the server belongs to category (1), even if there is no special hardware backup system.

You might also terminate and produce a post mortem dump to aid debugging of the problem. This is the best approach for dealing with programming errors. Unfulfilled pre-conditions, such as "index out of range" or "pop from empty stack", must generally be treated as programming errors - and what can you usefully do with a programming error other than to terminate the program and debug?

Our example probably falls into category (2); so, termination might be a useful solution. The readChar and the functions of DiagramBuilder would just terminate the program on detection of a problem. One way to terminate the problem is the EndProgram exception, which enables all functions on the stack to correctly clean up all held resources. For applications with resources that are not freed by the operating system this is one way to avoid resource leaks. But, this again introduces C++ exceptions into the system. A solution to avoid C++ exceptions is to register all external resources at a global place and to free them from the termination routine. Other duties of the termination routine are to inform other threads and/or processes about the termination. When the process is restarted by a process monitor, this monitor could be informed of open client connections etc. so that the restarted process could seamlessly fill in the place of the terminated one.

Review So Far

For the design of the example framework error handling, there are two different decisions to make:

  1. How to report errors of the framework to the application.

  2. How to enable the application classes derived from the framework classes to report problems.

In both cases, the application has to handle the problem, and not the framework. All the above mechanisms and standard C++ exception handling could be used as design solution. And, as we have seen, it might make sense to use different mechanisms for (1) and (2). And even for readChar and buildX, which are both of type (2), it could be useful to use different mechanisms. So, the designer of a truly generic framework should not setup a fixed error handling mechanism, but leave these decisions open to the application designer. How to do this easily, and how to solve other problems of all the above mechanisms as well, I will show you in the next Overload issue.

References

[GoF] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns, Addison-Wesley 1994, ISBN 0-201-63361-2

[comp.lang] In spring 96, there was a very interesting Usenet discussion "Safe use of exceptions: possible? worth the trouble?" in comp.lang.c++.moderated. The whole discussion is still very recommended to read. The thread started with Message-ID < 4ljuch$aap@netlab.cs.rpi.edu >

[Kanze1] James Kanze, above discussion, Message-ID < 4ncc5ua@netlab.cs.rpi.edu >

[Vandevoorde] David Vandevoorde, above discussion, Message-IDs < 4mlfh0$g@netlab.cs.rpi.edu > and < 4mr6fr$qqv@netlab.cs.rpi.edu >

[Kanze2] James Kanze, above discussion, Message-ID < 4n07r5$mkg@netlab.cs.rpi.edu >






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.