More About Error Handlers
Unless you elect to put all your error handling inline, you will find it necessary to design one or more error handlers for your DB Interface Module applications. So far, we have deliberately kept our example error handling routines extremely simple, in order to concentrate on presenting an overview of the error model. In this section, we go into the subject in more detail.
Kinds of Errors
The first thing a useful error handler needs to know is what kind of error it is dealing with. The Essential Tools Module approach is to classify errors as internal or external, depending on whether or not they can reasonably be predicted in advance by the programmer. In the Essential Tools Module, an internal error throws an exception of type
RWInternalErr, and an external error throws an exception of type
RWExternalErr. In the DB Interface Module, however, database errors are also possible. To simplify the situation, we have adopted the policy that
all errors are external errors. This means that:
• The type of exception thrown by
RWDBStatus::raise() is
RWExternalErr. Note that some of the Essential Tools Module classes used by the DB Interface Module could still throw an
RWInternalErr.
• Your application logic needs to distinguish between errors from databases and errors from the DB Interface Module.
The
errorCode() member function of
RWDBStatus tells you the kind of error you're dealing with; for a list of error codes, see the entry for
RWDBStatus in the
SourcePro API Reference Guide. Of these, the codes
vendorLib,
serverError, and
serverMessage indicate database errors or events, while the remaining error messages are generated by the DB Interface Module itself. But even though there are many more error codes generated by the DB Interface Module, our experience indicates that most errors in database applications are database errors. In order to distinguish database errors from other errors, the first part of our error handler might look like this:
void errorHandler(const RWDBStatus& status) {
RWDBStatus::ErrorCode code = status.errorCode();
if (code == RWDBStatus::vendorLib ||
code == RWDBStatus::serverError ||
code == RWDBStatus::serverMessage) {
// handle database error
}
else { // raise an exception
if (!status.isValid()) {
status.raise();
}
}
}
Capturing Database-Dependent Information
Once your application receives a database error, you must first interpret it before you can handle it. The main problem here is that each database vendor has its own ways of representing and categorizing errors. The DB Interface Module solves this problem by providing class
RWDBStatus with four variables reserved for database-dependent information. These are accessed using the member functions
vendorError1(),
vendorError2(),
vendorMessage1(),and
vendorMessage2(). The first two return
long int, and the second two return
RWCString.
Each DB Access Module defines the contents of
RWDBStatus for its particular database. Each Access Module user’s guide provides details. As comparative examples,
Table 9,
Table 10, and
Table 11 list the mappings used for three popular databases, Oracle, Sybase, and Microsoft SQL Server.
Table 9 – RWDBStatus contents for Oracle errors
Field | Contents |
errorCode | RWDBStatus::serverError if OCI_ERROR was received, otherwise RWDBStatus::serverMessage |
message | “[SERVERERROR] %s" or "[SERVERMESSAGE] %s", replacing %s with the error text from the OCIErrorGet() call |
vendorMessage1 | Unused |
vendorMessage2 | Unused |
vendorError1 | Error code from the OCIErrorGet() call |
vendorError2 | Unused |
Table 10 – RWDBStatus contents for Sybase Open Client Client-Library (and CS-Library) errors
Field | Contents |
errorCode | RWDBStatus::vendorLib, unless the severity is zero, as rated by Client-Library or CS-Library. If severity is zero, errorCode is set to RWDBStatus::ok, and the user-installed error handler is invoked. |
message | [VENDORLIB] Vendor Library Error:%s, where %s is the error text from Client-Library or CS-Library (CS_CLIENTMSG.msgstring) |
vendorMessage1 | The text reported by Client-Library or CS-Library as an operating system error, if any (CS_CLIENTMSG.osstring). |
vendorMessage2 | The text that describes the error (CS_CLIENTMSG.sqlstate). Not all client messages have state values associated with them. |
vendorError1 | Client-Library or CS-Library error number (CS_CLIENTMSG.msgnumber) |
vendorError2 | The error's severity level (CS_CLIENTMSG.severity). |
Table 11 – RWDBStatus contents for Microsoft SQL Server errors
Field | Contents |
errorCode | RWDBStatus::serverError if an error occurred, or RWDBStatus::ok if a warning is being generated, or RWDBStatus::notConnected if a communication error occurred indicated by a SQLSTATE code of 08XXX; for example, "08001" |
message | "SQL call failed" if an error occurred, or "Success with info" if a warning is being generated |
vendorMessage1 | The state parameter output from the SQLGetDiagRec() call; for example, "21S01" |
vendorMessage2 | The error message parameter output from the SQLGetDiagRec() call; for example, "[Microsoft][ODBC SQL Server Driver] Invalid cursor state" |
vendorError1 | The native error code parameter output from the call to SQLGetDiagRec() |
vendorError2 | The SQLRETURN code from the failed SQL function. |
You can see in these tables that the layouts are similar but not identical. This is not surprising, since error reporting is not standardized well among database APIs. Let us now return and modify our error handler to cope with this problem:
void errorHandler(const RWDBStatus& s) {
switch(s.errorCode()) {
case RWDBStatus::serverError:
cout << s.message() << endl
<< "Error number: " << s.vendorError1() << endl
<< "Server: " << s.vendorMessage1() << endl
<< "Severity: " << s.vendorError2() << endl;
break;
case RWDBStatus::serverMessage:
case RWDBStatus::vendorLib:
// handle other database errors
default:
// raise an exception
if (!status.isValid()) {
s.raise();
}
}
}
Now let's create an error condition. The following fragment attempts to drop a database table that doesn't exist:
RWDBTable junk = myDbase.table("junk");//table doesn't exist
RWDBStatus s = junk.drop();
On our system, this is the output when the example is run against an Oracle database and a Sybase database, respectively:
(Oracle output)
[SERVERERROR] ORA-00942: table or view
does not exist
Error number: 942
Server:
(Sybase output)
[SERVERERROR] Cannot drop the table 'junk',
because it doesn't exist in the system catalogs.
Error number: 3701
Server: SYBASE100
Severity: 11
Now suppose that your application needs to take some special action if a table doesn't exist. Evidently your application must be able to interpret database errors, not just report them, and to interpret them it must recognize the database. Perhaps the most straightforward way for your application to handle different databases, then, is to have separate error handlers for each database type:
const long OraNoTableError = 942;
const long SybNoTableError = 3701;
void oraErrorHandler(const RWDBStatus& s) {
RWDBStatus::ErrorCode code = s.errorCode();
if (code == RWDBStatus::serverError &&
s.vendorError1() == OraNoTableError) {
// take special action
}
else {
s.raise();
}
}
void sybErrorHandler(const RWDBStatus& s) {
RWDBStatus::ErrorCode code = s.errorCode();
if (code == RWDBStatus::serverError &&
s.vendorError1() == SybNoTableError) {
// take special action
}
else {
s.raise();
}
}
.
.
.
RWDBDatabase oracle = RWDBManager::database
( /*args for Oracle*/ );
RWDBDatabase sybase = RWDBManager::database
( /*args for Sybase*/ );
oracle.setErrorHandler(oraErrorHandler);
sybase.setErrorHandler(sybErrorHandler);
When the database-dependent table does not exist error comes up, it is trapped explicitly and handled by the take special action case in the appropriate error handler. The trade-off here is between portability and specificity. The more portable your application, the less database-dependent intelligence it can use, and vice versa.
To summarize, once you make the decision to interpret information in a database-dependent way, you can do it several different ways. In addition to the examples shown here, you may consider deriving from class
RWDBStatus in order to obtain a database-dependent interface to it. Or you might design a database exception class with derived database-dependent variants, and explicitly
throw() instances of your class instead of using the
raise() method of
RWDBStatus.