SeaQL/sea-orm · error

Fail to rollback transaction

Error message

Fail to rollback transaction

What it means

When a DatabaseTransaction is dropped without commit or explicit rollback, Drop calls start_rollback() and panics if rollback cannot be scheduled. This guarantees transactions are never silently leaked, but it turns an inability to roll back (e.g. connection already closed or the async runtime is shutting down) into a panic during unwinding/drop.

Source

Thrown at src/database/transaction.rs:404

        }
        Ok(())
    }
}

#[async_trait::async_trait]
impl TransactionSession for DatabaseTransaction {
    async fn commit(self) -> Result<(), DbErr> {
        self.commit().await
    }

    async fn rollback(self) -> Result<(), DbErr> {
        self.rollback().await
    }
}

impl Drop for DatabaseTransaction {
    fn drop(&mut self) {
        self.start_rollback().expect("Fail to rollback transaction");
    }
}

#[async_trait::async_trait]
impl ConnectionTrait for DatabaseTransaction {
    fn get_database_backend(&self) -> DbBackend {
        // this way we don't need to lock just to know the backend
        self.backend
    }

    #[instrument(level = "trace", skip(stmt))]
    #[allow(unused_variables)]
    async fn execute_raw(&self, stmt: Statement) -> Result<ExecResult, DbErr> {
        debug_print!("{}", stmt);

        super::tracing_spans::with_db_span!(
            "sea_orm.execute",
            self.backend,

View on GitHub (pinned to e29bcd1b41)

Solutions

  1. Always commit or roll back transactions explicitly instead of relying on Drop.
  2. Ensure the async runtime outlives all open transactions; do not drop transactions during runtime shutdown.
  3. Verify the connection is still alive (ping) before starting work that may end in an implicit rollback.
  4. If you must drop mid-failure, call .rollback().await yourself and handle its Result.

Example fix

// before
let txn = db.begin().await?;
do_work(&txn).await?;
// drop(txn) may panic on failed implicit rollback
// after
let txn = db.begin().await?;
match do_work(&txn).await {
    Ok(v) => txn.commit().await?,
    Err(e) => { txn.rollback().await.ok(); return Err(e); }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before relying on drop-rollback, ensure the connection is usable:
let alive = txn.get_database_backend().ping().is_ok(); // schedule explicit rollback if false

Type guard

fn txn_alive(txn: &DatabaseTransaction) -> bool { matches!(txn.ping().await, Ok(())) }

Try / catch

// Panics in Drop cannot be caught at the drop site; prevent instead:
let res = scopeguard::guard((), |_| {});
match body(&txn).await {
    Ok(v) => { txn.commit().await?; Ok(v) }
    Err(e) => { txn.rollback().await.ok(); Err(e) } // explicit, non-panicking rollback
}

Prevention

When it happens

Trigger: Dropping a DatabaseTransaction whose backing connection is broken, whose rollback future could not be spawned on the runtime, or dropping during process/runtime shutdown; also double-drop or drop after the connection was consumed elsewhere.

Common situations: Using ? to exit a scope without explicit rollback while the pool/connection is dead; tearing down a tokio runtime while transactions are still in scope; panics inside a transaction body causing drop-during-unwind.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of SeaQL/sea-orm@e29bcd1b41 (2026-09-10). Data as JSON: /api/errors/3fba1de60f65abe0. Report an issue: GitHub.