Skip to main content

Error Handling

All execution-level methods (transact(), transactRead(), execute(), run()) throw DatabaseException -- an unchecked exception. You never need throws declarations on your method signatures.

Sealed exception hierarchy

DatabaseException is a sealed class with three subtypes:

SubtypeCarriesWhen
DatabaseException.PostgresPgError with 17 structured fieldsPostgreSQL connections
DatabaseException.SqlServerSqlServerError with 7 structured fieldsSQL Server connections
DatabaseException.JdbcSQLExceptionAll other databases

All three share a sqlState() method on the base class, so you can handle errors generically or match on the specific subtype.

Pattern matching

Use Java 21 pattern matching to handle each database differently:

try {
insertUser.on(user).transact(tx);
} catch (DatabaseException.Postgres pg) {
if ("23505".equals(pg.sqlState())) {
String constraint = pg.pgError().constraintName();
// handle unique violation
}
} catch (DatabaseException.SqlServer ss) {
System.out.println("Error " + ss.sqlServerError().errorNumber());
} catch (DatabaseException.Jdbc e) {
System.out.println("SQL state: " + e.sqlState());
}

SQL state handling

When you only care about the error category and not the database, catch the base class and switch on sqlState():

try {
insertUser.on(user).transact(tx);
} catch (DatabaseException e) {
switch (e.sqlState()) {
case "23505" -> handleDuplicate();
case "23503" -> handleForeignKey();
default -> throw e;
}
}

Common SQL state classes:

ClassMeaning
"23"Integrity constraint violation
"42"Syntax error or access rule violation
"08"Connection exception
"40"Transaction rollback

PostgreSQL errors

DatabaseException.Postgres carries a PgError record with all fields from the PostgreSQL ErrorResponse wire protocol message:

The required fields are severity, message, and sqlState. Nullable fields include detail, hint, position, where, schemaName, tableName, columnName, dataTypeName, and constraintName. Additional internal fields (internalPosition, internalQuery, file, line, routine) are available for PL/pgSQL debugging.

Formatted error messages

getMessage() produces multi-line formatted output. For a syntax error with a position field, the output includes a caret pointing to the error location:

PostgreSQL syntax error with caret

For a unique constraint violation, detail and constraint fields are appended:

PostgreSQL unique constraint violation with structured fields

When the driver provides a hint, it's included along with the caret:

PostgreSQL column not found with hint

When a ResultSet value doesn't match the expected type at runtime, the error includes the column position, expected and actual types, the offending value, and the root cause:

Runtime parse error with detailed context

SQL Server errors

DatabaseException.SqlServer carries a SqlServerError record with fields from the TDS ERROR token:

FieldTypeDescription
message()@Nullable StringError message text
errorNumber()intVendor error number (e.g. 2627 for unique violation)
errorSeverity()intSeverity level (0-25)
errorState()intError state
serverName()@Nullable StringServer that raised the error
procedureName()@Nullable StringStored procedure name, if applicable
lineNumber()longLine number within the batch or procedure

Where checked exceptions remain

SQLException is still used in implementation interfaces -- SqlFunction, DbRead, DbWrite, ResultSetParser. These are where your code interacts with JDBC directly. The framework catches SQLException at the execution boundary and wraps it in the appropriate DatabaseException subtype.

Spring integration

Spring's @Transactional rolls back DatabaseException automatically since it is unchecked:

@Transactional
public void placeOrder(Order order) {
insertOrder.on(order).transact(tx);
updateInventory.on(order.itemId()).transact(tx);
}

See Spring Boot for full integration details.