Part II: Core CORBA
Chapter 4. The OMG Interface Definition Language
4.9 User Exceptions
IDL uses exceptions as a standard way to indicate error conditions. An IDL user exception is defined much like an IDL structure, and that allows an exception to contain an arbitrary amount of error information of arbitrary type. However, exceptions cannot be nested. Here is an example:
exception Failed {}; exception RangeError {
unsigned long min_permitted_val; unsigned long max_permitted_val; };
Exceptions, like structures, create a namespace, so the exception member names need be unique only within their enclosing exception.
Exceptions are types but cannot be used as data members of user-defined types. For example, the following is illegal:
struct ErrorReport { Object obj;
RangeError exc; // Error, exception as data member };
An operation uses a raises expression to indicate the exceptions it may possibly raise:
interface Unreliable {
void can_fail() raises(Failed);
void can_also_fail(in long l) raises(Failed, RangeError); };
As you can see, an operation may raise more than one type of exception. Operations must indicate all the exceptions they may possibly raise. It is illegal for an operation to throw a user exception that is not listed in the raises expression. A raises expression must not be empty.
IDL does not support exception inheritance. This means that you cannot arrange error conditions into logical hierarchies (as you can in C++) and catch all exceptions in a subtree by catching a base exception. Instead, every user exception creates a new type that is unrelated to any other exception type. This restriction exists because exception hierarchies using multiple inheritance are difficult to map to languages that do not support the concept directly. (Because exceptions have data members, the target language would have to support implementation inheritance.) However, single inheritance for exceptions could have been mapped quite easily, even to target languages that lack support for implementation inheritance.
Unfortunately, even single inheritance for exceptions did not make it into the initial OMG IDL specification, so we are stuck without it. (It is unlikely that exception inheritance will ever be added to OMG IDL because it would be disruptive to some language mappings.)
4.9.1 Exception Design Issues
When designing your interfaces, keep in mind that it is harder for a programmer to deal with exceptions than ordinary return values because exceptions break the normal flow of control. You should take some care in deciding whether something is an exception or a
return value. Consider the following interface, which provides a database lookup operation:
interface DB {
typedef sequence<Record> ResultSeq; typedef string QueryType;
exception NotFound { // Bad approach QueryType failed_query;
};
ResultSeq lookup(in QueryType query) raises(NotFound); };
The lookup operation in this interface returns a sequence of results in response to a passed query. If no matching records are found, it raises NotFound. There are a number of things wrong with this interface.
When searching a database, it is expected that a search will occasionally not locate anything. It is therefore inappropriate to raise an exception to indicate this. Instead, you should use a parameter or return value to indicate the empty result.
In the preceding example, raising an exception is redundant because you can indicate the empty result by returning an empty sequence. The NotFound exception complicates the interface unnecessarily.
The NotFound exception contains the failed_query member. Because only one query is passed to the operation, there is only one possible query that can fail—namely, the one that was passed to lookup. The exception contains information that is already known to the caller, and that is pointless.
The DB interface does not allow the caller to find out why a query failed. Was it because no records matched the query, or was it because the query contained a syntax error? Compare the preceding version with this one:
interface DB {
typedef sequence<Record> ResultSeq; typedef string QueryType; exception SyntaxError {
unsigned short position; };
ResultSeq lookup(in QueryType query) raises(SyntaxError); };
This version is almost identical to the previous one. However, the flaws are eliminated. A search that returns no results is indicated by returning an empty sequence instead of raising an exception.
An exception is raised if the query itself is unacceptable. This enables the caller to distinguish between a bad query and a query that merely did not return any results.
The exception contains useful information. In this case, it contains the index of the character position in the query string at which a syntax error was found.
The DB example highlights some lessons that many designers still refuse to heed. They can be summarized as follows.
Raise exceptions only for exceptional conditions.
Operations that raise exceptions for expected outcomes are ergonomically poor. Consider the programmer who needs to call such an operation. The C++ mapping maps IDL exceptions to C++ exceptions. C++ exceptions are harder to deal with than normal return values or parameters because exceptions break the normal flow of control. Forcing the programmer to catch an exception for expected behavior is simply bad style.
Make sure that exceptions carry useful information.
It is worse than useless to tell the caller something that is already known. Make sure that exceptions convey precise information.
An exception should convey precisely one semantic error condition. Do not lump several error conditions together so that the caller can no longer distinguish between them.
Make sure that exceptions carry complete information.
If exceptions carry incomplete information, the caller will probably need to make further calls to find out what exactly went wrong. If the initial call did not work, there is a good chance that subsequent calls will also fail, and that can make precise error handling impossible for the caller.
Design interfaces so that they cater to the needs of the caller and not the needs of the implementer.
Computing abounds with difficult-to-use APIs that provide poor abstractions of functionality. Typically, such APIs come into existence because they are written by the implementer of the functionality and not its user. But good tools are built for the convenience of the tool user; the effort required by the tool maker to create the tool is usually considered irrelevant (within reason). APIs are tools, and you should build them to suit their users.
Do not use normal return values or parameters to indicate errors.
As you will see in the next section, operations can raise exceptions even if they do not have a raises expression. If you use error codes instead of exceptions, callers end up with inconsistent and convoluted error handling because they must check for exceptions as well as an error return code.