Skip to content

Error handling

The query APIs do not throw: they return a Result, and a failure holds an SQLError. Call getOrThrow() to throw it (as the examples do), or inspect it with onFailure, fold, exceptionOrNull(), … With the Arrow repositories, the generated methods return an Either<SQLError, T> instead.

val res: Result<Long> = db.execute("delete from users where id = 1;")
res.onFailure { e -> println("Delete failed: ${e.message}") }

The cancellation of the calling coroutine is never turned into a failed Result: it is rethrown, as usual with coroutines.

SQLError

Every SQLError carries a code, the failure from the library's point of view. When the database itself rejected the statement (code Database), it also carries what the database said about it:

Property Set on What it holds
code every error The sqlx4k error code: Database, PoolTimedOut, OptimisticLockFailed, … (see Error codes).
kind every error A portable classification of the database error, the same on every database and platform (see Error kinds). Other when not mapped.
sqlState PostgreSQL, MySQL The five-character SQLSTATE of the SQL standard, e.g. 23505 (unique violation) on PostgreSQL. null on SQLite, which has none.
nativeCode MySQL, SQLite The database's own error number: the server error on MySQL (e.g. 1062, ER_DUP_ENTRY), the extended result code on SQLite (e.g. 2067). null on PostgreSQL.
cause when a lower layer failed The original exception, e.g. the driver's exception on the JVM.

Error kinds

kind covers what application code usually branches on, without parsing database-specific codes: the integrity constraint violations (report them to the user) and the concurrency failures (retry the transaction).

Kind Meaning
UniqueViolation A unique or primary key constraint was violated.
ForeignKeyViolation A foreign key constraint was violated.
NotNullViolation A not-null constraint was violated.
CheckViolation A check constraint was violated.
ExclusionViolation An exclusion constraint was violated (PostgreSQL only).
Deadlock The transaction was chosen as a deadlock victim (PostgreSQL 40P01, MySQL 1213).
SerializationFailure The transaction could not be serialized with concurrent ones (PostgreSQL 40001).
LockTimeout A lock could not be acquired in time (PostgreSQL 55P03, MySQL 1205, SQLite busy or locked).
Other Not a database error, or one that is not mapped: use sqlState and nativeCode.

For instance, to report a duplicate email:

val statement = Statement
    .create("insert into users(name, email) values (:name, :email)")
    .bind("name", "Alice")
    .bind("email", "alice@example.com")

db.execute(statement).onFailure { e ->
    when ((e as? SQLError)?.kind) {
        SQLError.Kind.UniqueViolation -> println("The email is already taken.")
        else -> throw e
    }
}

Or to retry a transaction that lost to a concurrent one:

suspend fun <T> retrying(attempts: Int = 3, block: suspend () -> T): T {
    repeat(attempts - 1) {
        try {
            return block()
        } catch (e: SQLError) {
            if (e.kind != SQLError.Kind.Deadlock && e.kind != SQLError.Kind.SerializationFailure) throw e
        }
    }
    return block()
}

val total: Long = retrying {
    db.transaction {
        execute("update accounts set balance = balance - 10 where id = 1;").getOrThrow()
        execute("update accounts set balance = balance + 10 where id = 2;").getOrThrow()
    }
}

Note

Constraint violations are classified by the driver itself. The concurrency failures are derived from the SQLSTATE on PostgreSQL, the error number on MySQL and the result code on SQLite, with the same mapping on the native and the JVM drivers. On the SQLite driver for Android, nativeCode is recovered from the exception message, and is therefore best-effort.

Error codes

Code When
Database The database rejected the statement: see kind, sqlState and nativeCode.
PoolTimedOut No connection became available within the pool's acquireTimeout (see Connection pool).
PoolClosed The pool was closed.
Pool A connection could not be opened, e.g. the server refused it or the TLS handshake failed.
WorkerCrashed The driver's background worker failed.
ConnectionIsClosed The connection was used after it was closed (returned to the pool).
TransactionIsClosed The transaction was used after its commit or rollback.
TransactionCommitFailed, TransactionRollbackFailed The commit or the rollback of a transaction failed.
CannotDecode A NULL column was read with a function that does not allow it, e.g. asInt() (see Reading values).
CannotDecodeEnumValue asEnum<T>() read a value that is not the name of a constant of T.
PositionalParameterOutOfBounds, NamedParameterNotFound A value was bound to a parameter the statement does not have.
PositionalParameterValueNotSupplied, NamedParameterValueNotSupplied A parameter of the statement was left without a value.
MissingValueConverter A bound value has a type the driver cannot send: register a value converter.
EmptyCollection An empty collection was bound (see Binding collections).
InvalidIdentifier, UnsafeStringContent A name or a string the library writes into SQL itself (e.g. a savepoint or a migration table) was rejected.
Migrate A migration failed: a duplicate or missing version, a changed or empty file, …
EmptyResultSet A generated repository method expected a row and got none, e.g. delete of an entity that does not exist.
MultipleRowsReturned A generated findOneBy… method matched more than one row.
RowMismatch A generated batchInsert or batchUpdate affected a different number of rows than the number of entities.
OptimisticLockFailed A versioned entity was modified or deleted since it was read (see Optimistic locking).
UnknownError Any other failure.