Exception.h

Detailed Description

An Exception indicates an error condition from which recovery may be possible.

The Library raises exceptions, which can be handled by recovery code, if recovery is possible. When an exception is raised, it is handled by the handler that was most recently instantiated. If no handlers are defined an exception will cause the library to call its abort handler to abort with an error message.

Handlers are instantiated by the TRY-CATCH and TRY-FINALLY statements, which are implemented as macros in this interface. These statements handle nested exceptions and manage exception-state data. The syntax of the TRY-CATCH statement is,

S
CATCH(e1)
S1
CATCH(e2)
S2
[...]
CATCH(en)
Sn
#define CATCH(e)
Defines a block containing code for handling an exception thrown in the TRY block.
Definition Exception.h:449
#define TRY
Defines a block of code that can potentially throw an exception.
Definition Exception.h:433
#define END_TRY
Ends a TRY-CATCH block.
Definition Exception.h:483

The TRY-CATCH statement establishes handlers for the exceptions named e1, e2,.., en and execute the statements S. If no exceptions are raised by S, the handlers are dismantled and execution continues at the statement after the END_TRY. If S raises an exception e which is one of e1..en the execution of S is interrupted and control transfers immediately to the statements following the relevant CATCH clause. If S raises an exception that is not one of e1..en, the exception will raise up the call-stack and unless a previous installed handler catch the exception, it will cause the application to abort.

The TRY-FINALLY statement is similar to TRY-CATCH but in addition adds a FINALLY clause which is always executed, regardless if an exception was raised or not. The syntax of the TRY-FINALLY statement is,

S
CATCH(e1)
S1
CATCH(e2)
S2
[...]
CATCH(en)
Sn
Sf
#define FINALLY
Defines a block of code that is subsequently executed whether an exception is thrown or not.
Definition Exception.h:472

Note that Sf is executed whether S raises an exception or not. One purpose of the TRY-FINALLY statement is to give clients an opportunity to "clean up" when an exception occurs. For example,

{
}
{
}
void Connection_close(T C)
Returns the connection to the connection pool.
void Connection_execute(T C, const char *sql,...)
Executes a SQL statement, with or without parameters.

closes the database Connection regardless if an exception was thrown or not by the code in the TRY-block. The above example also demonstrates that FINALLY can be used without an exception handler, if an exception was thrown it will be rethrown after the control reaches the end of the finally block. Meaning that we can cleanup even if an exception was thrown and the exception will automatically propagate up the call stack afterwards.

Finally, the RETURN statement, defined in this interface, must be used instead of C return statements inside a try-block. If any of the statements in a try block must do a return, they must do so with this macro instead of the usual C return statement.

Recommended: Use TRY-ELSE

For most use cases, we recommend using TRY-ELSE rather than TRY-CATCH. The ELSE block catches any exception, which simplifies client code since libzdb can throw various Exception types. Unless you need to differentiate between specific exception types, TRY-ELSE provides a cleaner and more robust pattern:

{
}
{
// Handle error
}
#define ELSE
Defines a block containing code for handling any exception thrown in the TRY block.
Definition Exception.h:461

Use TRY-CATCH only when you need to handle different exception types differently. For general error handling where the response is the same regardless of the exception type, TRY-ELSE is the preferred approach.

Exception details

Inside an exception handler, details about an exception are available in the variable Exception_frame. The following demonstrates usage of this variable to provide detailed logging of an exception. For SQL errors, Connection_getLastError() can also be used, though Exception_frame is recommended since in addition to SQL errors, it also covers API errors not directly related to SQL.

{
<code that can throw an exception>
}
{
fprintf(stderr, "%s: %s raised in %s at %s:%d\n",
Exception_frame.exception->name,
Exception_frame.message,
Exception_frame.func,
Exception_frame.file,
Exception_frame.line);
}

Error codes

In addition to the exception message, Exception_frame.errorCode provides the numeric error code from the underlying database when available. This allows for more robust error handling. For example, to handle a MySQL deadlock:

{
}
{
if (Exception_frame.errorCode == ER_LOCK_DEADLOCK) {
// Retry the transaction
} else {
log("Database error %d: %s\n",
Exception_frame.errorCode,
Exception_frame.message);
}
}

The error code is database-specific; consult your database's documentation for the meaning of specific codes (e.g., MySQL error codes, PostgreSQL SQLSTATE values, etc.). A value of 0 typically indicates no specific error code was provided.

Database-Specific Error Codes

The error code in Exception_frame.errorCode is database-specific. Each database backend provides error codes in their own format:

PostgreSQL

PostgreSQL uses SQLSTATE codes, a five-character standard defined by ISO/IEC 9075. libzdb encodes these as integers in SQLState.h.

if (Exception_frame.errorCode == SQLSTATE_unique_violation) {
// Handle duplicate key (SQLSTATE 23505)
} else if (Exception_frame.errorCode == SQLSTATE_foreign_key_violation) {
// Handle FK violation (SQLSTATE 23503)
} else if (Exception_frame.errorCode == SQLSTATE_deadlock_detected) {
// Handle deadlock - consider retry (SQLSTATE 40P01)
} else if (Exception_frame.errorCode == SQLSTATE_lock_not_available) {
// Handle lock timeout - consider retry (SQLSTATE 55P03)
}
#define SQLSTATE_foreign_key_violation
Definition SQLState.h:251
#define SQLSTATE_unique_violation
Definition SQLState.h:252
#define SQLSTATE_lock_not_available
Definition SQLState.h:399
#define SQLSTATE_deadlock_detected
Definition SQLState.h:331

For debugging, use SQLState_toString() to convert the code back to its standard 5-character representation:

char sqlstate[6];
SQLState_toString(Exception_frame.errorCode, sqlstate);
printf("SQLSTATE: %s\n", sqlstate); // e.g., "40P01"
char * SQLState_toString(int code, char *buf)
Decode an integer SQLSTATE back to its string representation.

See: https://www.postgresql.org/docs/current/errcodes-appendix.html

MySQL/MariaDB

MySQL error codes are native integers from mysql_errno(). Common codes are defined in MySQL's errmsg.h and mysqld_error.h headers. Examples:

  • 1062 (ER_DUP_ENTRY) - Duplicate entry for key
  • 1213 (ER_LOCK_DEADLOCK) - Deadlock found
  • 1205 (ER_LOCK_WAIT_TIMEOUT) - Lock wait timeout
  • 1452 (ER_NO_REFERENCED_ROW_2) - Foreign key constraint fails
#include <mysqld_error.h> // If available
if (Exception_frame.errorCode == 1062) { // ER_DUP_ENTRY
// Handle duplicate key
}

See: https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html

SQLite

SQLite uses extended error codes from sqlite3_extended_errcode(). These are defined in sqlite3.h. Examples:

  • SQLITE_CONSTRAINT_UNIQUE (2067) - UNIQUE constraint failed
  • SQLITE_CONSTRAINT_PRIMARYKEY (1555) - PRIMARY KEY constraint failed
  • SQLITE_CONSTRAINT_FOREIGNKEY (787) - FOREIGN KEY constraint failed
  • SQLITE_BUSY (5) - Database is locked
  • SQLITE_LOCKED (6) - Database table is locked
#include <sqlite3.h>
if (Exception_frame.errorCode == SQLITE_CONSTRAINT_UNIQUE) {
// Handle duplicate key
}

See: https://www.sqlite.org/rescode.html

Oracle

Oracle uses ORA- error numbers (the numeric portion without the ORA- prefix). Common codes include:

  • 1 (ORA-00001) - Unique constraint violated
  • 60 (ORA-00060) - Deadlock detected while waiting for resource
  • 1017 (ORA-01017) - Invalid username/password
  • 1400 (ORA-01400) - Cannot insert NULL
  • 1438 (ORA-01438) - Value larger than allowed precision
  • 2291 (ORA-02291) - Integrity constraint violated - parent key not found
  • 2292 (ORA-02292) - Integrity constraint violated - child record found
if (Exception_frame.errorCode == 1) {
// Handle unique constraint violation (ORA-00001)
} else if (Exception_frame.errorCode == 60) {
// Handle deadlock - consider retry (ORA-00060)
}

See: https://docs.oracle.com/en/database/oracle/oracle-database/19/errmg/

Volatile and assignment inside a try-block

A variable declared outside a try-block and assigned a value inside said block should be declared volatile if the variable will be accessed from an exception handler. Otherwise the compiler will/may optimize away the value set in the try-block and the handler will not see the new value. Declaring the variable volatile is only necessary if the variable is to be used inside a CATCH or ELSE block. Example:

volatile int i = 0;
{
i = 1;
THROW(SQLException, "SQLException");
}
{
assert(i == 1); // Unless declared volatile i would be 0 here
}
assert(i == 1); // i will be 1 here regardless if it is declared volatile or not
Exception_T SQLException

Thread-safe

The Exception stack is stored in a thread-specific variable so Exceptions are made thread-safe. This means that Exceptions are thread local and an Exception thrown in one thread cannot be caught in another thread. This also means that clients must handle Exceptions per thread and cannot use one TRY-ELSE block in the main program to catch all Exceptions. This is only possible if no threads were started.

This implementation is a minor modification of the Except code found in David R. Hanson's excellent book C Interfaces and Implementations.

See also
SQLException.h

Macros

#define T   Exception_T
#define THROW(e, cause, ...)
 Throws an exception.
#define THROW_SQL(errorCode, cause, ...)
 Throws an SQLException with a database error code.
#define RETHROW
 Re-throws an exception.
#define RETURN
 Clients must use this macro instead of C return statements inside a try-block.
#define TRY
 Defines a block of code that can potentially throw an exception.
#define CATCH(e)
 Defines a block containing code for handling an exception thrown in the TRY block.
#define ELSE
 Defines a block containing code for handling any exception thrown in the TRY block.
#define FINALLY
 Defines a block of code that is subsequently executed whether an exception is thrown or not.
#define END_TRY
 Ends a TRY-CATCH block.

Macro Definition Documentation

◆ T

#define T   Exception_T

◆ THROW

#define THROW ( e,
cause,
... )

Throws an exception.

Parameters
eThe Exception to throw
causeThe cause. A NULL value is permitted, and indicates that the cause is unknown.

◆ THROW_SQL

#define THROW_SQL ( errorCode,
cause,
... )

Throws an SQLException with a database error code.

Parameters
errorCodeThe SQL Error Code
causeThe cause. A NULL value is permitted, and indicates that the cause is unknown.

◆ RETHROW

#define RETHROW

Re-throws an exception.

In a CATCH or ELSE block clients can use RETHROW to re-throw the Exception

◆ RETURN

#define RETURN

Clients must use this macro instead of C return statements inside a try-block.

◆ TRY

#define TRY

Defines a block of code that can potentially throw an exception.

◆ CATCH

#define CATCH ( e)

Defines a block containing code for handling an exception thrown in the TRY block.

Parameters
eThe Exception to handle

◆ ELSE

#define ELSE

Defines a block containing code for handling any exception thrown in the TRY block.

An ELSE block catches any exception type not already caught in a previous CATCH block.

◆ FINALLY

#define FINALLY

Defines a block of code that is subsequently executed whether an exception is thrown or not.

◆ END_TRY

#define END_TRY

Ends a TRY-CATCH block.

Copyright © Tildeslash Ltd. All rights reserved.