tursodatabase/turso · error

Transaction dropped unexpectedly.

Error message

Transaction dropped unexpectedly.

What it means

Panic from the Turso Rust binding's dangling-transaction handler. When a Transaction is dropped without commit or rollback, its DropBehavior is stored on the connection and resolved lazily by the next query/execute/prepare (maybe_handle_dangling_tx in connection.rs). With DropBehavior::Panic the resolution deliberately panics so a leaked transaction is never silent.

Source

Thrown at bindings/rust/src/connection.rs:117

    }

    pub(crate) async fn maybe_handle_dangling_tx(&self) -> Result<()> {
        match self.dangling_tx.load(Ordering::SeqCst) {
            DropBehavior::Rollback => {
                let mut stmt = self.prepare("ROLLBACK").await?;
                stmt.execute(()).await?;
                self.dangling_tx
                    .store(DropBehavior::Ignore, Ordering::SeqCst);
            }
            DropBehavior::Commit => {
                let mut stmt = self.prepare("COMMIT").await?;
                stmt.execute(()).await?;
                self.dangling_tx
                    .store(DropBehavior::Ignore, Ordering::SeqCst);
            }
            DropBehavior::Ignore => {}
            DropBehavior::Panic => {
                panic!("Transaction dropped unexpectedly.");
            }
        }
        Ok(())
    }

    /// Query the database with SQL.
    pub async fn query(&self, sql: impl AsRef<str>, params: impl IntoParams) -> Result<Rows> {
        self.maybe_handle_dangling_tx().await?;
        let mut stmt = self.prepare(sql).await?;
        stmt.query(params).await
    }

    /// Execute SQL statement on the database.
    pub async fn execute(&self, sql: impl AsRef<str>, params: impl IntoParams) -> Result<u64> {
        self.maybe_handle_dangling_tx().await?;
        let mut stmt = self.prepare(sql).await?;
        stmt.execute(params).await
    }

View on GitHub (pinned to c1e5928725)

Solutions

  1. Explicitly tx.commit().await or tx.rollback().await on every path (match or scope guard) before the Transaction can drop
  2. Call tx.finish().await to resolve the transaction consciously under the current behavior
  3. Switch to DropBehavior::Rollback (default) or Ignore while you fix the leak
  4. Restrict DropBehavior::Panic to tests where the panic itself is the assertion

Example fix

// before
let tx = conn.begin().await?;
let v = do_work(&tx).await?; // early return drops tx
// ... later conn.query(..) panics: "Transaction dropped unexpectedly."

// after
let tx = conn.begin().await?;
match do_work(&tx).await {
    Ok(v) => { tx.commit().await?; Ok(v) }
    Err(e) => { tx.rollback().await?; Err(e) }
}
Defensive patterns

Strategy: validation

Validate before calling

// guarantee an explicit outcome before the transaction can drop
pub async fn with_tx<T>(conn: &Connection, f: impl FnOnce(&Transaction<'_>) -> BoxFuture<'_, Result<T>>) -> Result<T> {
    let tx = conn.begin().await?;
    match f(&tx).await {
        Ok(v) => { tx.commit().await?; Ok(v) }
        Err(e) => { let _ = tx.rollback().await; Err(e) }
    }
}

Try / catch

// it is a panic, not a Result; isolate only if unavoidable
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    futures::executor::block_on(conn.query("SELECT 1"))
}));

Prevention

When it happens

Trigger: Calling conn.begin(), letting the Transaction drop (early `?` return, cancelled task, dropped future) while DropBehavior::Panic is stored, then issuing any conn.query()/execute()/prepare() - those entry points call maybe_handle_dangling_tx first and hit the panic arm.

Common situations: Teams enabling Panic as a strict-discipline mode to catch missing commits; `?` operators between begin() and the end of the scope; timeouts or task cancellation dropping the future that owned the transaction.

Related errors


AI-assisted analysis of tursodatabase/turso@c1e5928725 (2026-08-20). Data as JSON: /api/errors/40807fac1761a496. Report an issue: GitHub.