tursodatabase/turso · error · SqliteException

SqliteCommand.ToSqliteException(ex)

Error message

SqliteCommand.ToSqliteException(ex)

What it means

The SqliteTransaction constructor asks the managed connection to BEGIN a transaction (BEGIN IMMEDIATE for serializable non-deferred transactions, otherwise BEGIN). If the backend rejects the BEGIN (e.g. a transaction is already active or the connection failed), the TursoException is converted to a SqliteException. The deferred fallback path uses plain text 'BEGIN;' via Execute.

Source

Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteTransaction.cs:33

    {
        _connection = connection;
        _isolationLevel = NormalizeIsolationLevel(connection, isolationLevel, deferred);

        if (_isolationLevel == IsolationLevel.ReadUncommitted)
            connection.ReadUncommitted = true;

        if (connection.IsManagedConnection)
        {
            try
            {
                _managedTransaction = new global::Turso.TursoTransaction(
                    connection.ManagedConnection,
                    _isolationLevel,
                    deferred);
            }
            catch (Turso.Raw.Public.TursoException ex)
            {
                throw SqliteCommand.ToSqliteException(ex);
            }

            return;
        }

        Execute(_isolationLevel == IsolationLevel.Serializable && !deferred ? "BEGIN IMMEDIATE;" : "BEGIN;");
    }

    public override IsolationLevel IsolationLevel => _isolationLevel;

    public override bool SupportsSavepoints => true;

    protected override DbConnection? DbConnection => Connection;

    public new virtual SqliteConnection? Connection => _connection;

    internal bool IsCompleted => _completed;

View on GitHub (pinned to 6c72522679)

Solutions

  1. Ensure the previous SqliteTransaction is committed, rolled back, or disposed before starting a new one on the same connection.
  2. Use one transaction at a time per connection; don't share a SqliteConnection across threads without synchronization.
  3. Catch SqliteException and check for SQLITE_BUSY (5) to implement retry/backoff on locked databases.
  4. If available, enable connection pooling per logical operation instead of manual nested transactions.

Example fix

// before
var tx = conn.BeginTransaction();
var tx2 = conn.BeginTransaction(); // throws: transaction already active
// after
using (var tx = conn.BeginTransaction())
{
    // ...
    tx.Commit();
} // dispose ends the transaction before another can begin
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure no transaction is active before beginning a new one
if (conn.EnlistedTransaction != null || _currentTx != null)
    throw new InvalidOperationException("transaction already active on this connection");

Type guard

static bool CanBeginTransaction(SqliteConnection conn) => conn.State == ConnectionState.Open;

Try / catch

try
{
    using var tx = conn.BeginTransaction(IsolationLevel.ReadCommitted);
    // ... work, tx.Commit()
}
catch (SqliteException ex) when (ex.SqliteErrorCode == 5)
{
    // SQLITE_BUSY: retry with backoff
}
catch (SqliteException ex)
{
    logger.LogError(ex, "BEGIN failed");
    throw;
}

Prevention

When it happens

Trigger: calling conn.BeginTransaction() while another transaction is already open on the same connection; connection to remote Turso server is broken; backend rejects BEGIN IMMEDIATE because the database is locked.

Common situations: Nested transaction attempts (BeginTransaction inside a using-block whose transaction wasn't committed/disposed), parallel use of one connection from multiple threads, busy database under contention.

Understand the failure class

Background: "query failed", "%w: SQL error" — wrapped database query errors in Go libraries explained — this error's family across 3 libraries.

Related errors


AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-09-06). Data as JSON: /api/errors/4e960259faaeb560. Report an issue: GitHub.